@l1yp/file-viewer 0.1.1 → 0.1.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.
Files changed (431) hide show
  1. package/README.md +1 -0
  2. package/dist/FileViewer.js +2 -0
  3. package/dist/FileViewer.js.map +1 -0
  4. package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  5. package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  6. package/dist/components/AppxViewer.js +2 -0
  7. package/dist/components/AppxViewer.js.map +1 -0
  8. package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  9. package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  10. package/dist/components/ArchiveBrowser.js +2 -0
  11. package/dist/components/ArchiveBrowser.js.map +1 -0
  12. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  13. package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  14. package/dist/components/AudioViewer.js +2 -0
  15. package/dist/components/AudioViewer.js.map +1 -0
  16. package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  17. package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  18. package/dist/components/BinObjBrowser.js +2 -0
  19. package/dist/components/BinObjBrowser.js.map +1 -0
  20. package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  21. package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  22. package/dist/components/CertViewer.js +2 -0
  23. package/dist/components/CertViewer.js.map +1 -0
  24. package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  25. package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  26. package/dist/components/ClassViewer.js +2 -0
  27. package/dist/components/ClassViewer.js.map +1 -0
  28. package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  29. package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  30. package/dist/components/CodeBlock.js +2 -0
  31. package/dist/components/CodeBlock.js.map +1 -0
  32. package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
  33. package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
  34. package/dist/components/CodeView.js +2 -0
  35. package/dist/components/CodeView.js.map +1 -0
  36. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +2 -0
  37. package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -0
  38. package/dist/components/CsvViewer.js +2 -0
  39. package/dist/components/CsvViewer.js.map +1 -0
  40. package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  41. package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  42. package/dist/components/DicomViewer.js +2 -0
  43. package/dist/components/DicomViewer.js.map +1 -0
  44. package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  45. package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  46. package/dist/components/DocxNavigationPane.js +2 -0
  47. package/dist/components/DocxNavigationPane.js.map +1 -0
  48. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
  49. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  50. package/dist/components/DocxViewer.js +2 -0
  51. package/dist/components/DocxViewer.js.map +1 -0
  52. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  53. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  54. package/dist/components/DrawioViewer.js +2 -0
  55. package/dist/components/DrawioViewer.js.map +1 -0
  56. package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  57. package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  58. package/dist/components/DxfViewer.js +2 -0
  59. package/dist/components/DxfViewer.js.map +1 -0
  60. package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  61. package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  62. package/dist/components/EbookViewer.js +2 -0
  63. package/dist/components/EbookViewer.js.map +1 -0
  64. package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  65. package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  66. package/dist/components/EmlViewer.js +2 -0
  67. package/dist/components/EmlViewer.js.map +1 -0
  68. package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  69. package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  70. package/dist/components/EplViewer.js +2 -0
  71. package/dist/components/EplViewer.js.map +1 -0
  72. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  73. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  74. package/dist/components/FileViewerBody.js +2 -0
  75. package/dist/components/FileViewerBody.js.map +1 -0
  76. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +2 -0
  77. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -0
  78. package/dist/components/FileViewerPanel.js +2 -0
  79. package/dist/components/FileViewerPanel.js.map +1 -0
  80. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +2 -0
  81. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -0
  82. package/dist/components/FontViewer.js +2 -0
  83. package/dist/components/FontViewer.js.map +1 -0
  84. package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  85. package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  86. package/dist/components/FrontmatterCard.js +2 -0
  87. package/dist/components/FrontmatterCard.js.map +1 -0
  88. package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js +2 -0
  89. package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js.map +1 -0
  90. package/dist/components/GeoViewer.js +2 -0
  91. package/dist/components/GeoViewer.js.map +1 -0
  92. package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  93. package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  94. package/dist/components/HarViewer.js +2 -0
  95. package/dist/components/HarViewer.js.map +1 -0
  96. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  97. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  98. package/dist/components/HexViewer.js +2 -0
  99. package/dist/components/HexViewer.js.map +1 -0
  100. package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  101. package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  102. package/dist/components/HprofViewer.js +2 -0
  103. package/dist/components/HprofViewer.js.map +1 -0
  104. package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  105. package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  106. package/dist/components/ImageLightbox.js +2 -0
  107. package/dist/components/ImageLightbox.js.map +1 -0
  108. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +2 -0
  109. package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -0
  110. package/dist/components/InlineText.js +2 -0
  111. package/dist/components/InlineText.js.map +1 -0
  112. package/dist/components/JksViewer.js +2 -0
  113. package/dist/components/JksViewer.js.map +1 -0
  114. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  115. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  116. package/dist/components/JsonlViewer.js +2 -0
  117. package/dist/components/JsonlViewer.js.map +1 -0
  118. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  119. package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  120. package/dist/components/LogViewer.js +2 -0
  121. package/dist/components/LogViewer.js.map +1 -0
  122. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  123. package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  124. package/dist/components/LrcViewer.js +2 -0
  125. package/dist/components/LrcViewer.js.map +1 -0
  126. package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  127. package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  128. package/dist/components/MarkdownTable.js +2 -0
  129. package/dist/components/MarkdownTable.js.map +1 -0
  130. package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js +2 -0
  131. package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
  132. package/dist/components/MarkdownText.js +2 -0
  133. package/dist/components/MarkdownText.js.map +1 -0
  134. package/dist/components/MhtViewer.js +2 -0
  135. package/dist/components/MhtViewer.js.map +1 -0
  136. package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  137. package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  138. package/dist/components/MindMapView.js +2 -0
  139. package/dist/components/MindMapView.js.map +1 -0
  140. package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js +2 -0
  141. package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js.map +1 -0
  142. package/dist/components/MissingDependency.js +2 -0
  143. package/dist/components/MissingDependency.js.map +1 -0
  144. package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js +2 -0
  145. package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js.map +1 -0
  146. package/dist/components/ModelViewer.js +2 -0
  147. package/dist/components/ModelViewer.js.map +1 -0
  148. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  149. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  150. package/dist/components/MsiViewer.js +2 -0
  151. package/dist/components/MsiViewer.js.map +1 -0
  152. package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  153. package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  154. package/dist/components/NotebookViewer.js +2 -0
  155. package/dist/components/NotebookViewer.js.map +1 -0
  156. package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  157. package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  158. package/dist/components/PdfViewer.js +2 -0
  159. package/dist/components/PdfViewer.js.map +1 -0
  160. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  161. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  162. package/dist/components/PptxNavigationPane.js +2 -0
  163. package/dist/components/PptxNavigationPane.js.map +1 -0
  164. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
  165. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
  166. package/dist/components/PptxViewer.js +3 -1
  167. package/dist/components/PptxViewer.js.map +1 -0
  168. package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js +55 -49
  169. package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  170. package/dist/components/PsdViewer.js +2 -0
  171. package/dist/components/PsdViewer.js.map +1 -0
  172. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  173. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  174. package/dist/components/QmcAudioViewer.js +2 -0
  175. package/dist/components/QmcAudioViewer.js.map +1 -0
  176. package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  177. package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  178. package/dist/components/RasterViewer.js +2 -0
  179. package/dist/components/RasterViewer.js.map +1 -0
  180. package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  181. package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  182. package/dist/components/RtfViewer.js +2 -0
  183. package/dist/components/RtfViewer.js.map +1 -0
  184. package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  185. package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  186. package/dist/components/SevenZipBrowser.js +2 -0
  187. package/dist/components/SevenZipBrowser.js.map +1 -0
  188. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  189. package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  190. package/dist/components/SqliteViewer.js +2 -0
  191. package/dist/components/SqliteViewer.js.map +1 -0
  192. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  193. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  194. package/dist/components/SubtitleViewer.js +2 -0
  195. package/dist/components/SubtitleViewer.js.map +1 -0
  196. package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  197. package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  198. package/dist/components/XlsxChart.js +2 -0
  199. package/dist/components/XlsxChart.js.map +1 -0
  200. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +2 -0
  201. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -0
  202. package/dist/components/XlsxViewer.js +2 -0
  203. package/dist/components/XlsxViewer.js.map +1 -0
  204. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  205. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  206. package/dist/components/XmindViewer.js +2 -0
  207. package/dist/components/XmindViewer.js.map +1 -0
  208. package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js +2 -0
  209. package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  210. package/dist/components/ZipBrowser.js +2 -0
  211. package/dist/components/ZipBrowser.js.map +1 -0
  212. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
  213. package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
  214. package/dist/components/ZoomImage.js +2 -0
  215. package/dist/components/ZoomImage.js.map +1 -0
  216. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +2 -0
  217. package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -0
  218. package/dist/components/epl/EplCodeBlock.js +2 -0
  219. package/dist/components/epl/EplCodeBlock.js.map +1 -0
  220. package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
  221. package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
  222. package/dist/components/epl/EplDoc.js +2 -0
  223. package/dist/components/epl/EplDoc.js.map +1 -0
  224. package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js +2 -0
  225. package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js.map +1 -0
  226. package/dist/components/epl/EplMethod.js +2 -0
  227. package/dist/components/epl/EplMethod.js.map +1 -0
  228. package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js +2 -0
  229. package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js.map +1 -0
  230. package/dist/components/epl/EplResource.js +2 -0
  231. package/dist/components/epl/EplResource.js.map +1 -0
  232. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js +2 -0
  233. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -0
  234. package/dist/components/epl/EplTree.js +2 -0
  235. package/dist/components/epl/EplTree.js.map +1 -0
  236. package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js +2 -0
  237. package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js.map +1 -0
  238. package/dist/components/epl/EplVarTable.js +2 -0
  239. package/dist/components/epl/EplVarTable.js.map +1 -0
  240. package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js +2 -0
  241. package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
  242. package/dist/components/epl/numbering.js +2 -0
  243. package/dist/components/epl/numbering.js.map +1 -0
  244. package/dist/components/icons/Icon.js +2 -0
  245. package/dist/components/icons/Icon.js.map +1 -0
  246. package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js +2 -0
  247. package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js.map +1 -0
  248. package/dist/components/icons/iconPaths.js +2 -0
  249. package/dist/components/icons/iconPaths.js.map +1 -0
  250. package/dist/components/optionalViewer.js +2 -0
  251. package/dist/components/optionalViewer.js.map +1 -0
  252. package/dist/components/ui/ClampBlock.js +2 -0
  253. package/dist/components/ui/ClampBlock.js.map +1 -0
  254. package/dist/composables/useElementNavigation.js +2 -0
  255. package/dist/composables/useElementNavigation.js.map +1 -0
  256. package/dist/composables/useFileViewer.js +2 -0
  257. package/dist/composables/useFileViewer.js.map +1 -0
  258. package/dist/composables/useLightbox.js +2 -0
  259. package/dist/composables/useLightbox.js.map +1 -0
  260. package/dist/composables/useLogRules.js +2 -0
  261. package/dist/composables/useLogRules.js.map +1 -0
  262. package/dist/lib/aes.js +2 -0
  263. package/dist/lib/aes.js.map +1 -0
  264. package/dist/lib/appx.js +2 -0
  265. package/dist/lib/appx.js.map +1 -0
  266. package/dist/lib/archiveHtml.js +2 -0
  267. package/dist/lib/archiveHtml.js.map +1 -0
  268. package/dist/lib/asn1.js +2 -0
  269. package/dist/lib/asn1.js.map +1 -0
  270. package/dist/lib/binobj.js +2 -0
  271. package/dist/lib/binobj.js.map +1 -0
  272. package/dist/lib/brotli.js +2 -0
  273. package/dist/lib/brotli.js.map +1 -0
  274. package/dist/lib/bzip2.js +2 -0
  275. package/dist/lib/bzip2.js.map +1 -0
  276. package/dist/lib/classfile.js +2 -0
  277. package/dist/lib/classfile.js.map +1 -0
  278. package/dist/lib/codePreview.js +2 -0
  279. package/dist/lib/codePreview.js.map +1 -0
  280. package/dist/lib/codefold.js +2 -0
  281. package/dist/lib/codefold.js.map +1 -0
  282. package/dist/lib/codeformat.js +2 -0
  283. package/dist/lib/codeformat.js.map +1 -0
  284. package/dist/lib/csv.js +2 -0
  285. package/dist/lib/csv.js.map +1 -0
  286. package/dist/lib/des.js +2 -0
  287. package/dist/lib/des.js.map +1 -0
  288. package/dist/lib/diagram.js +2 -0
  289. package/dist/lib/diagram.js.map +1 -0
  290. package/dist/lib/dicom.js +2 -0
  291. package/dist/lib/dicom.js.map +1 -0
  292. package/dist/lib/dicomCodecs.js +2 -0
  293. package/dist/lib/dicomCodecs.js.map +1 -0
  294. package/dist/lib/dicomRender.js +2 -0
  295. package/dist/lib/dicomRender.js.map +1 -0
  296. package/dist/lib/docxNavigation.js +2 -0
  297. package/dist/lib/docxNavigation.js.map +1 -0
  298. package/dist/lib/dxf.js +2 -0
  299. package/dist/lib/dxf.js.map +1 -0
  300. package/dist/lib/eml.js +2 -0
  301. package/dist/lib/eml.js.map +1 -0
  302. package/dist/lib/encoding.js +2 -0
  303. package/dist/lib/encoding.js.map +1 -0
  304. package/dist/lib/epl/code/codeDataParser.js +2 -0
  305. package/dist/lib/epl/code/codeDataParser.js.map +1 -0
  306. package/dist/lib/epl/code/codeLines.js +2 -0
  307. package/dist/lib/epl/code/codeLines.js.map +1 -0
  308. package/dist/lib/epl/code/textCode.js +2 -0
  309. package/dist/lib/epl/code/textCode.js.map +1 -0
  310. package/dist/lib/epl/eplDocument.js +2 -0
  311. package/dist/lib/epl/eplDocument.js.map +1 -0
  312. package/dist/lib/epl/imageList.js +2 -0
  313. package/dist/lib/epl/imageList.js.map +1 -0
  314. package/dist/lib/epl/index.js +2 -0
  315. package/dist/lib/epl/index.js.map +1 -0
  316. package/dist/lib/epl/io/binaryReader.js +2 -0
  317. package/dist/lib/epl/io/binaryReader.js.map +1 -0
  318. package/dist/lib/epl/io/encoding.js +2 -0
  319. package/dist/lib/epl/io/encoding.js.map +1 -0
  320. package/dist/lib/epl/model/eplSystemId.js +2 -0
  321. package/dist/lib/epl/model/eplSystemId.js.map +1 -0
  322. package/dist/lib/epl/names/eplLibData.js +2 -0
  323. package/dist/lib/epl/names/eplLibData.js.map +1 -0
  324. package/dist/lib/epl/names/idToNameMap.js +2 -0
  325. package/dist/lib/epl/names/idToNameMap.js.map +1 -0
  326. package/dist/lib/epl/names/libNameProvider.js +2 -0
  327. package/dist/lib/epl/names/libNameProvider.js.map +1 -0
  328. package/dist/lib/epl/program.js +2 -0
  329. package/dist/lib/epl/program.js.map +1 -0
  330. package/dist/lib/epl/projectFileReader.js +2 -0
  331. package/dist/lib/epl/projectFileReader.js.map +1 -0
  332. package/dist/lib/fb2.js +2 -0
  333. package/dist/lib/fb2.js.map +1 -0
  334. package/dist/lib/fileType.js +2 -0
  335. package/dist/lib/fileType.js.map +1 -0
  336. package/dist/lib/fonts.js +2 -0
  337. package/dist/lib/fonts.js.map +1 -0
  338. package/dist/lib/format.js +2 -0
  339. package/dist/lib/format.js.map +1 -0
  340. package/dist/lib/frontmatter.js +2 -0
  341. package/dist/lib/frontmatter.js.map +1 -0
  342. package/dist/lib/geo.js +2 -0
  343. package/dist/lib/geo.js.map +1 -0
  344. package/dist/lib/har.js +2 -0
  345. package/dist/lib/har.js.map +1 -0
  346. package/dist/lib/highlight.js +2 -0
  347. package/dist/lib/highlight.js.map +1 -0
  348. package/dist/lib/highlightLangs.js +2 -0
  349. package/dist/lib/highlightLangs.js.map +1 -0
  350. package/dist/lib/hprof.js +2 -0
  351. package/dist/lib/hprof.js.map +1 -0
  352. package/dist/lib/ipynb.js +2 -0
  353. package/dist/lib/ipynb.js.map +1 -0
  354. package/dist/lib/jks.js +2 -0
  355. package/dist/lib/jks.js.map +1 -0
  356. package/dist/lib/jsonl.js +2 -0
  357. package/dist/lib/jsonl.js.map +1 -0
  358. package/dist/lib/lrc.js +2 -0
  359. package/dist/lib/lrc.js.map +1 -0
  360. package/dist/lib/lzma.js +2 -0
  361. package/dist/lib/lzma.js.map +1 -0
  362. package/dist/lib/markdown/blocks.js +2 -0
  363. package/dist/lib/markdown/blocks.js.map +1 -0
  364. package/dist/lib/markdown/inline.js +2 -0
  365. package/dist/lib/markdown/inline.js.map +1 -0
  366. package/dist/lib/markdown/math.js +2 -0
  367. package/dist/lib/markdown/math.js.map +1 -0
  368. package/dist/lib/markdown/segmentize.js +2 -0
  369. package/dist/lib/markdown/segmentize.js.map +1 -0
  370. package/dist/lib/markdown/styles.js +2 -0
  371. package/dist/lib/markdown/styles.js.map +1 -0
  372. package/dist/lib/mht.js +2 -0
  373. package/dist/lib/mht.js.map +1 -0
  374. package/dist/lib/midi.js +2 -0
  375. package/dist/lib/midi.js.map +1 -0
  376. package/dist/lib/mime.js +2 -0
  377. package/dist/lib/mime.js.map +1 -0
  378. package/dist/lib/mindmap.js +2 -0
  379. package/dist/lib/mindmap.js.map +1 -0
  380. package/dist/lib/mobi.js +2 -0
  381. package/dist/lib/mobi.js.map +1 -0
  382. package/dist/lib/msi.js +2 -0
  383. package/dist/lib/msi.js.map +1 -0
  384. package/dist/lib/optionalDep.js +2 -0
  385. package/dist/lib/optionalDep.js.map +1 -0
  386. package/dist/lib/pkcs12.js +2 -0
  387. package/dist/lib/pkcs12.js.map +1 -0
  388. package/dist/lib/pptxNavigation.js +2 -0
  389. package/dist/lib/pptxNavigation.js.map +1 -0
  390. package/dist/lib/qmc.js +2 -0
  391. package/dist/lib/qmc.js.map +1 -0
  392. package/dist/lib/rar.js +2 -0
  393. package/dist/lib/rar.js.map +1 -0
  394. package/dist/lib/rawPreview.js +2 -0
  395. package/dist/lib/rawPreview.js.map +1 -0
  396. package/dist/lib/rc2.js +2 -0
  397. package/dist/lib/rc2.js.map +1 -0
  398. package/dist/lib/resolveInput.js +2 -0
  399. package/dist/lib/resolveInput.js.map +1 -0
  400. package/dist/lib/rtf.js +2 -0
  401. package/dist/lib/rtf.js.map +1 -0
  402. package/dist/lib/sevenZip.js +2 -0
  403. package/dist/lib/sevenZip.js.map +1 -0
  404. package/dist/lib/sha1.js +2 -0
  405. package/dist/lib/sha1.js.map +1 -0
  406. package/dist/lib/sha256.js +2 -0
  407. package/dist/lib/sha256.js.map +1 -0
  408. package/dist/lib/speechAudio.js +2 -0
  409. package/dist/lib/speechAudio.js.map +1 -0
  410. package/dist/lib/sqlite.js +2 -0
  411. package/dist/lib/sqlite.js.map +1 -0
  412. package/dist/lib/subtitle.js +2 -0
  413. package/dist/lib/subtitle.js.map +1 -0
  414. package/dist/lib/tarball.js +2 -0
  415. package/dist/lib/tarball.js.map +1 -0
  416. package/dist/lib/viewerFormats.js +2 -0
  417. package/dist/lib/viewerFormats.js.map +1 -0
  418. package/dist/lib/viewerKind.js +2 -0
  419. package/dist/lib/viewerKind.js.map +1 -0
  420. package/dist/lib/x509.js +2 -0
  421. package/dist/lib/x509.js.map +1 -0
  422. package/dist/lib/xlsx.js +2 -0
  423. package/dist/lib/xlsx.js.map +1 -0
  424. package/dist/lib/xlsxCharts.js +2 -0
  425. package/dist/lib/xlsxCharts.js.map +1 -0
  426. package/dist/lib/xmind.js +2 -0
  427. package/dist/lib/xmind.js.map +1 -0
  428. package/dist/lib/zip.js +2 -0
  429. package/dist/lib/zip.js.map +1 -0
  430. package/dist/style.css +1 -1
  431. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CertViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/CertViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'\nimport { fileExt } from '@/lib/fileType'\nimport { formatBytes } from '@/lib/format'\nimport {\n asn1String,\n asn1Time,\n bytesToHex,\n decodeAsn1,\n oidToString,\n tagName,\n TAG,\n type Asn1Node,\n} from '@/lib/asn1'\nimport {\n looksLikePem,\n oidName,\n parseCertificate,\n parseCsr,\n parsePemBlocks,\n sha256Hex,\n summarizeBlock,\n type CertSummary,\n} from '@/lib/x509'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Side-viewer for certificates / keys (pem / crt / cer / der / csr / key / p12 / pfx). Fetches the BYTES\n * itself (binary-safe — a .der/.p12 would be mangled by the panel's text load), sniffs PEM vs raw DER, and\n * for each block shows the human-meaningful X.509 fields (subject / issuer, validity with an expiry badge,\n * serial, signature + public-key algorithm, SANs, key usages, SHA-256 fingerprint) plus a collapsible raw\n * ASN.1 structure tree. Parsing is hand-rolled and READ-ONLY (lib/asn1.ts + lib/x509.ts) — it never verifies\n * signatures or trust. PKCS#12 (.p12/.pfx) containers are usually encrypted, so they're shown as structure\n * only with a \"use openssl to extract\" note (best effort).\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ninterface CertItem {\n summary: CertSummary\n forest: Asn1Node[]\n}\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst items = ref<CertItem[]>([])\nconst fingerprints = reactive<Record<number, string>>({})\nconst expanded = reactive(new Set<string>())\n\nconst SIZE_LIMIT = 8 * 1024 * 1024\n\nfunction buildItem(summary: CertSummary): CertItem {\n return { summary, forest: decodeAsn1(summary.der) }\n}\n\nfunction processBuffer(buf: ArrayBuffer): void {\n const bytes = new Uint8Array(buf)\n const ext = fileExt(props.name)\n const head = new TextDecoder('latin1').decode(bytes.subarray(0, Math.min(64, bytes.length)))\n const list: CertItem[] = []\n\n if (head.includes('-----BEGIN') || looksLikePem(new TextDecoder('latin1').decode(bytes))) {\n const text = new TextDecoder('utf-8').decode(bytes)\n for (const block of parsePemBlocks(text))\n list.push(buildItem(summarizeBlock(block.label, block.der)))\n }\n if (!list.length) {\n // Raw DER (or a PEM that yielded no blocks). Pick a parser from the extension.\n if (ext === 'p12' || ext === 'pfx') {\n const s = summarizeP12(bytes)\n list.push(buildItem(s))\n } else if (ext === 'csr' || ext === 'req') {\n list.push(buildItem(parseCsr(bytes)))\n } else {\n const cert = parseCertificate(bytes)\n list.push(buildItem(cert.ok ? cert : fallbackStructure(bytes, ext)))\n }\n }\n\n items.value = list\n status.value = 'ready'\n // Fingerprint each certificate asynchronously (SHA-256 of its DER).\n list.forEach((it, i) => {\n if (it.summary.kind === 'certificate' && it.summary.ok) {\n void sha256Hex(it.summary.der)\n .then((hex) => {\n fingerprints[i] = hex\n })\n .catch(() => {})\n }\n })\n // Open the first level of the first item so there's something to see without a click.\n if (list.length && list[0].forest.length) expanded.add('0.0')\n}\n\nfunction summarizeP12(bytes: Uint8Array): CertSummary {\n return {\n ok: true,\n kind: 'pkcs12',\n label: 'PKCS#12',\n subject: [],\n issuer: [],\n sans: [],\n keyUsages: [],\n extKeyUsages: [],\n der: bytes,\n note: 'PKCS#12 容器通常已加密,浏览器内仅展示 ASN.1 结构;如需提取证书/私钥请用 openssl pkcs12 -info。',\n }\n}\n\nfunction fallbackStructure(bytes: Uint8Array, ext: string): CertSummary {\n return {\n ok: true,\n kind: 'unknown',\n label: ext ? `.${ext}` : '二进制',\n subject: [],\n issuer: [],\n sans: [],\n keyUsages: [],\n extKeyUsages: [],\n der: bytes,\n note: '无法识别为 X.509 证书 —— 展开下方结构树查看原始 ASN.1。',\n }\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n items.value = []\n expanded.clear()\n for (const k of Object.keys(fingerprints)) delete fingerprints[Number(k)]\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n processBuffer(await res.arrayBuffer())\n } catch (e) {\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\n// ── friendly-field formatting ────────────────────────────────────────────────────────────────────\nfunction formatDn(rdns: CertSummary['subject']): string {\n return rdns.map((r) => `${r.name}=${r.value}`).join(', ')\n}\n\nconst KIND_LABEL: Record<CertSummary['kind'], string> = {\n certificate: '证书',\n request: '证书请求 (CSR)',\n key: '密钥',\n crl: 'CRL',\n pkcs12: 'PKCS#12',\n pkcs7: 'PKCS#7',\n unknown: '未知',\n}\n\nfunction publicKeyText(s: CertSummary): string {\n const parts: string[] = []\n if (s.pubKeyAlg) parts.push(s.pubKeyAlg)\n if (s.pubKeyBits) parts.push(`${s.pubKeyBits} 位`)\n if (s.pubKeyCurve && s.pubKeyCurve !== s.pubKeyAlg) parts.push(s.pubKeyCurve)\n return parts.join(' · ')\n}\n\n// Validity badge: future / expired / soon (≤30 days) / valid, from notBefore/notAfter vs now.\nfunction expiry(s: CertSummary): { text: string; cls: string } | null {\n if (s.notAfterMs == null) return null\n const now = Date.now()\n if (s.notBeforeMs != null && now < s.notBeforeMs)\n return { text: '尚未生效', cls: 'cert__badge--warn' }\n if (now > s.notAfterMs) return { text: '已过期', cls: 'cert__badge--bad' }\n const days = Math.floor((s.notAfterMs - now) / 86_400_000)\n if (days <= 30) return { text: `${days} 天后过期`, cls: 'cert__badge--warn' }\n return { text: `有效(剩 ${days} 天)`, cls: 'cert__badge--ok' }\n}\n\n// ── ASN.1 structure tree (flattened with an expand set) ──────────────────────────────────────────\ninterface TreeRow {\n node: Asn1Node\n depth: number\n path: string\n hasChildren: boolean\n}\n\nfunction flatten(forest: Asn1Node[], prefix: string): TreeRow[] {\n const rows: TreeRow[] = []\n const walk = (nodes: Asn1Node[], depth: number, pre: string): void => {\n nodes.forEach((node, i) => {\n const path = `${pre}.${i}`\n const hasChildren = node.children.length > 0\n rows.push({ node, depth, path, hasChildren })\n if (hasChildren && expanded.has(path)) walk(node.children, depth + 1, path)\n })\n }\n walk(forest, 0, prefix)\n return rows\n}\n\nconst treeRows = computed(() => items.value.map((it, i) => flatten(it.forest, String(i))))\n\nfunction toggleNode(path: string): void {\n if (expanded.has(path)) expanded.delete(path)\n else expanded.add(path)\n}\n\n// A short, human value preview for a primitive node (constructed nodes show only their tag).\nfunction nodePreview(node: Asn1Node): string {\n if (node.constructed) return ''\n switch (node.tagNumber) {\n case TAG.OID: {\n const oid = oidToString(node.content)\n const nm = oidName(oid)\n return nm === oid ? oid : `${nm} (${oid})`\n }\n case TAG.BOOLEAN:\n return node.content[0] ? 'true' : 'false'\n case TAG.INTEGER: {\n if (node.content.length <= 4) {\n let v = 0\n for (const b of node.content) v = v * 256 + b\n return String(v)\n }\n return `0x${bytesToHex(node.content).slice(0, 32)}${node.content.length > 16 ? '…' : ''}`\n }\n case TAG.UTF8String:\n case TAG.PrintableString:\n case TAG.IA5String:\n return asn1String(node)\n case TAG.UTCTime:\n case TAG.GeneralizedTime:\n return asn1Time(node)?.text ?? asn1String(node)\n case TAG.NULL:\n return ''\n default: {\n if (node.tagClass === 2) {\n // context-specific primitive (e.g. a SAN name) — show as text if printable, else hex\n const txt = new TextDecoder('latin1').decode(node.content)\n if (/^[\\x20-\\x7e]*$/.test(txt)) return txt\n }\n const hex = bytesToHex(node.content)\n return hex.length > 32 ? `${hex.slice(0, 32)}… (${node.length}B)` : hex\n }\n }\n}\n\nwatch(() => [props.url, props.name], load, { immediate: true })\nonBeforeUnmount(() => {\n // nothing to revoke; bytes are not retained as object URLs\n})\n</script>\n\n<template>\n <div class=\"cert\">\n <p v-if=\"status === 'loading'\" class=\"cert__msg\">正在解析证书…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"cert__msg cert__gate\">\n <p>该文件较大({{ formatBytes(props.size) }}),将在浏览器内解析。是否继续?</p>\n <button type=\"button\" class=\"cert__btn\" @click=\"doLoad\">解析并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"cert__msg cert__msg--error\">\n <p>无法解析:{{ errorMsg }}</p>\n <button type=\"button\" class=\"cert__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else class=\"cert__scroll\">\n <section v-for=\"(it, i) in items\" :key=\"i\" class=\"cert__card\">\n <header class=\"cert__cardhead\">\n <span class=\"cert__kind\">{{ KIND_LABEL[it.summary.kind] }}</span>\n <span class=\"cert__label\">{{ it.summary.subjectCN || it.summary.label }}</span>\n <span v-if=\"expiry(it.summary)\" class=\"cert__badge\" :class=\"expiry(it.summary)?.cls\">\n {{ expiry(it.summary)?.text }}\n </span>\n </header>\n\n <p v-if=\"it.summary.note\" class=\"cert__note\">{{ it.summary.note }}</p>\n\n <dl v-if=\"it.summary.subject.length || it.summary.kind === 'certificate'\" class=\"cert__dl\">\n <template v-if=\"it.summary.subject.length\">\n <dt>主题</dt>\n <dd>{{ formatDn(it.summary.subject) }}</dd>\n </template>\n <template v-if=\"it.summary.issuer.length\">\n <dt>颁发者</dt>\n <dd>{{ formatDn(it.summary.issuer) }}</dd>\n </template>\n <template v-if=\"it.summary.notBefore || it.summary.notAfter\">\n <dt>有效期</dt>\n <dd>{{ it.summary.notBefore ?? '?' }} → {{ it.summary.notAfter ?? '?' }}</dd>\n </template>\n <template v-if=\"publicKeyText(it.summary)\">\n <dt>公钥</dt>\n <dd>{{ publicKeyText(it.summary) }}</dd>\n </template>\n <template v-if=\"it.summary.sigAlg\">\n <dt>签名算法</dt>\n <dd>{{ it.summary.sigAlg }}</dd>\n </template>\n <template v-if=\"it.summary.serialHex\">\n <dt>序列号</dt>\n <dd class=\"cert__mono\">{{ it.summary.serialHex }}</dd>\n </template>\n <template v-if=\"it.summary.sans.length\">\n <dt>备用名 (SAN)</dt>\n <dd>{{ it.summary.sans.join(',') }}</dd>\n </template>\n <template v-if=\"it.summary.keyUsages.length\">\n <dt>密钥用法</dt>\n <dd>{{ it.summary.keyUsages.join(',') }}</dd>\n </template>\n <template v-if=\"it.summary.extKeyUsages.length\">\n <dt>扩展用法</dt>\n <dd>{{ it.summary.extKeyUsages.join(',') }}</dd>\n </template>\n <template v-if=\"it.summary.isCa != null\">\n <dt>CA</dt>\n <dd>\n {{ it.summary.isCa ? '是' : '否'\n }}{{ it.summary.pathLen != null ? `(pathLen ${it.summary.pathLen})` : '' }}\n </dd>\n </template>\n <template v-if=\"fingerprints[i]\">\n <dt>SHA-256</dt>\n <dd class=\"cert__mono cert__fp\">{{ fingerprints[i] }}</dd>\n </template>\n </dl>\n\n <details class=\"cert__tree\">\n <summary class=\"cert__treehead\">ASN.1 结构</summary>\n <div class=\"cert__rows\">\n <button\n v-for=\"row in treeRows[i]\"\n :key=\"row.path\"\n type=\"button\"\n class=\"cert__row\"\n :style=\"{ paddingLeft: `${8 + row.depth * 16}px` }\"\n @click=\"row.hasChildren && toggleNode(row.path)\"\n >\n <span class=\"cert__twisty\">\n <Icon\n v-if=\"row.hasChildren\"\n name=\"chevronRight\"\n :size=\"12\"\n :class=\"{ 'cert__twisty--open': expanded.has(row.path) }\"\n />\n </span>\n <span class=\"cert__tag\">{{ tagName(row.node) }}</span>\n <span v-if=\"nodePreview(row.node)\" class=\"cert__val\">{{\n nodePreview(row.node)\n }}</span>\n </button>\n </div>\n </details>\n </section>\n </div>\n </div>\n</template>\n\n<style scoped>\n.cert {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.cert__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.cert__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.cert__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.cert__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.cert__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.cert__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 14px 14px 60px;\n}\n\n.cert__card {\n margin-bottom: 16px;\n border: 1px solid var(--border);\n border-radius: 10px;\n background: var(--surface);\n overflow: hidden;\n}\n.cert__cardhead {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border);\n}\n.cert__kind {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--accent);\n background: color-mix(in srgb, var(--accent) 12%, transparent);\n border-radius: 5px;\n padding: 2px 7px;\n}\n.cert__label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.cert__badge {\n flex-shrink: 0;\n font-size: 11px;\n font-weight: 600;\n border-radius: 5px;\n padding: 2px 8px;\n}\n.cert__badge--ok {\n color: #15803d;\n background: color-mix(in srgb, #22c55e 16%, transparent);\n}\n.cert__badge--warn {\n color: #b45309;\n background: color-mix(in srgb, #f59e0b 18%, transparent);\n}\n.cert__badge--bad {\n color: #b91c1c;\n background: color-mix(in srgb, #ef4444 16%, transparent);\n}\n\n.cert__note {\n margin: 0;\n padding: 9px 12px;\n font-size: 12px;\n line-height: 1.5;\n color: var(--muted);\n background: color-mix(in srgb, var(--accent) 5%, transparent);\n border-bottom: 1px solid var(--border);\n}\n\n.cert__dl {\n display: grid;\n grid-template-columns: max-content 1fr;\n gap: 6px 14px;\n margin: 0;\n padding: 12px;\n}\n.cert__dl dt {\n font-size: 12px;\n color: var(--faint);\n white-space: nowrap;\n}\n.cert__dl dd {\n margin: 0;\n font-size: 12.5px;\n color: var(--text);\n word-break: break-word;\n}\n.cert__mono {\n font-family: var(--font-mono);\n font-size: 11.5px;\n}\n.cert__fp {\n word-break: break-all;\n}\n\n.cert__tree {\n border-top: 1px solid var(--border);\n}\n.cert__treehead {\n padding: 8px 12px;\n font-size: 12px;\n color: var(--muted);\n cursor: pointer;\n user-select: none;\n}\n.cert__treehead:hover {\n color: var(--accent);\n}\n.cert__rows {\n padding: 4px 0 10px;\n background: var(--code-bg);\n border-top: 1px solid var(--code-border);\n overflow-x: auto;\n}\n.cert__row {\n display: flex;\n align-items: center;\n gap: 7px;\n width: 100%;\n padding: 3px 12px 3px 8px;\n background: transparent;\n border: none;\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text);\n text-align: left;\n white-space: nowrap;\n cursor: default;\n}\n.cert__row:hover {\n background: var(--surface-2);\n}\n.cert__twisty {\n flex-shrink: 0;\n display: inline-flex;\n width: 12px;\n color: var(--faint);\n}\n.cert__twisty--open {\n transform: rotate(90deg);\n transition: transform 0.12s;\n}\n.cert__tag {\n flex-shrink: 0;\n color: var(--accent);\n}\n.cert__val {\n min-width: 0;\n color: var(--muted);\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./ClassViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-31d96cf6"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=ClassViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClassViewer.js","names":[],"sources":["../../src/components/ClassViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport {\n parseClass,\n NotClassFileError,\n type ClassInfo,\n type ClassLocal,\n type ClassMethod,\n} from '@/lib/classfile'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's Java `.class` mode: fetch the bytes, parse the structure entirely in-browser (see\n * lib/classfile.ts), and list the FIELDS and METHODS — a lightweight `javap`. Mirrors BinObjBrowser: the\n * bytes come from the same download endpoint as any other file and never leave the browser. No decompilation\n * (method bodies aren't reconstructed); a method's local variables expand inline when the class carried a\n * LocalVariableTable (`javac -g`).\n */\nconst props = defineProps<{ url: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'error' | 'ready'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst info = ref<ClassInfo | null>(null)\nconst openMethods = ref<Set<number>>(new Set())\n\nasync function load(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n info.value = null\n openMethods.value = new Set()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n info.value = parseClass(await res.arrayBuffer())\n status.value = 'ready'\n } catch (e) {\n errorMsg.value =\n e instanceof NotClassFileError ? e.message : e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// Short type for display (drop the package); the full name stays in the title.\nfunction shortType(t: string | null): string {\n if (!t) return ''\n const dot = t.replace(/\\[\\]$/g, '').lastIndexOf('.')\n return dot >= 0 ? t.slice(dot + 1) : t\n}\n// `<init>` / `<clinit>` get friendly labels; everything else is its own name.\nfunction methodLabel(m: ClassMethod): string {\n if (m.name === '<init>') return '«构造器»'\n if (m.name === '<clinit>') return '«静态初始化»'\n return m.name\n}\nfunction realLocals(m: ClassMethod): ClassLocal[] {\n return (m.locals ?? []).filter((l) => l.name !== 'this') // slot 0 of an instance method is `this`\n}\nfunction toggleMethod(i: number): void {\n const s = new Set(openMethods.value)\n if (s.has(i)) s.delete(i)\n else s.add(i)\n openMethods.value = s\n}\n\nconst signature = computed(() => {\n const c = info.value\n if (!c) return ''\n const head = [...c.modifiers, c.kind, c.thisClass ?? '?'].join(' ')\n return head\n})\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => openMethods.value.clear())\n</script>\n\n<template>\n <div class=\"cls\">\n <p v-if=\"status === 'loading'\" class=\"cls__msg\">正在解析 .class…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"cls__msg cls__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"cls__retry\" @click=\"load\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && info\">\n <div class=\"cls__summary\">\n <span class=\"cls__kind\">{{ info.kind.toUpperCase() }}</span>\n <span class=\"cls__ver\">字节码 v{{ info.version }}</span>\n <span>{{ info.javaLabel }}</span>\n <span>{{ info.fields.length }} 字段</span>\n <span>{{ info.methods.length }} 方法</span>\n </div>\n\n <!-- signature card: `modifiers kind ThisClass extends Super implements I, J` -->\n <div class=\"cls__sig\">\n <code class=\"cls__sigline\">\n <span class=\"cls__mod\">{{ signature }}</span>\n <template v-if=\"info.superClass && info.superClass !== 'java.lang.Object'\">\n <span class=\"cls__kw\"> extends </span\n ><span class=\"cls__type\" :title=\"info.superClass\">{{\n shortType(info.superClass)\n }}</span>\n </template>\n <template v-if=\"info.interfaces.length\">\n <span class=\"cls__kw\">\n {{\n info.kind === 'interface' || info.kind === '@interface' ? 'extends' : 'implements'\n }}\n </span>\n <span v-for=\"(it, i) in info.interfaces\" :key=\"it\">\n <span v-if=\"i > 0\" class=\"cls__punc\">, </span\n ><span class=\"cls__type\" :title=\"it\">{{ shortType(it) }}</span>\n </span>\n </template>\n </code>\n </div>\n\n <!-- FIELDS -->\n <section class=\"cls__section\">\n <h3 class=\"cls__h\">字段 · FIELDS</h3>\n <p v-if=\"!info.fields.length\" class=\"cls__empty\">无字段</p>\n <div v-else class=\"cls__rows\">\n <div v-for=\"(f, i) in info.fields\" :key=\"i\" class=\"cls__member\">\n <span v-if=\"f.flags.length\" class=\"cls__mods\">{{ f.flags.join(' ') }}</span>\n <span class=\"cls__type\" :title=\"f.type\">{{ shortType(f.type) }}</span>\n <span class=\"cls__name\">{{ f.name }}</span>\n </div>\n </div>\n </section>\n\n <!-- METHODS -->\n <section class=\"cls__section\">\n <h3 class=\"cls__h\">方法 · METHODS</h3>\n <p v-if=\"!info.methods.length\" class=\"cls__empty\">无方法</p>\n <div v-else class=\"cls__rows\">\n <div v-for=\"(m, i) in info.methods\" :key=\"i\" class=\"cls__methodwrap\">\n <component\n :is=\"realLocals(m).length ? 'button' : 'div'\"\n :type=\"realLocals(m).length ? 'button' : undefined\"\n class=\"cls__member cls__method\"\n :class=\"{ 'cls__method--btn': realLocals(m).length }\"\n :title=\"realLocals(m).length ? '点击查看局部变量' : undefined\"\n @click=\"realLocals(m).length && toggleMethod(i)\"\n >\n <span\n v-if=\"realLocals(m).length\"\n class=\"cls__caret\"\n :class=\"{ 'cls__caret--open': openMethods.has(i) }\"\n >▶</span\n >\n <span v-if=\"m.flags.length\" class=\"cls__mods\">{{ m.flags.join(' ') }}</span>\n <span class=\"cls__type\" :title=\"m.ret\">{{ shortType(m.ret) }}</span>\n <span class=\"cls__name\" :class=\"{ 'cls__name--special': m.name.startsWith('<') }\">{{\n methodLabel(m)\n }}</span>\n <span class=\"cls__params\">\n <span class=\"cls__punc\">(</span\n ><template v-for=\"(pt, pi) in m.params\" :key=\"pi\"\n ><span v-if=\"pi > 0\" class=\"cls__punc\">, </span\n ><span class=\"cls__type\" :title=\"pt\">{{ shortType(pt) }}</span></template\n ><span class=\"cls__punc\">)</span>\n </span>\n </component>\n\n <div v-if=\"openMethods.has(i)\" class=\"cls__locals\">\n <span class=\"cls__locals-label\">局部变量</span>\n <div v-for=\"(l, li) in realLocals(m)\" :key=\"li\" class=\"cls__local\">\n <span class=\"cls__type\" :title=\"l.type\">{{ shortType(l.type) }}</span>\n <span class=\"cls__localname\">{{ l.name }}</span>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <div class=\"cls__foot\">\n <Icon name=\"code\" :size=\"13\" />\n <span>仅解析结构(字段 / 方法),不反编译方法体</span>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.cls {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface);\n}\n\n.cls__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.cls__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.cls__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.cls__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Summary bar (mirrors BinObjBrowser). */\n.cls__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px 12px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.cls__kind {\n font-weight: 700;\n color: var(--accent);\n}\n.cls__ver {\n font-variant-numeric: tabular-nums;\n}\n\n/* Signature card. */\n.cls__sig {\n flex-shrink: 0;\n padding: 12px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n}\n.cls__sigline {\n font-family: var(--font-mono);\n font-size: 13px;\n line-height: 1.6;\n word-break: break-word;\n}\n.cls__mod {\n color: var(--text-strong);\n font-weight: 600;\n}\n.cls__kw {\n color: var(--muted);\n}\n\n.cls__section {\n flex-shrink: 0;\n padding: 12px 0 6px;\n border-bottom: 1px solid var(--border);\n}\n.cls__h {\n margin: 0 0 6px;\n padding: 0 16px;\n font-size: 10.5px;\n font-weight: 700;\n letter-spacing: 0.08em;\n color: var(--faint);\n}\n.cls__empty {\n margin: 0;\n padding: 2px 16px 6px;\n font-size: 12px;\n color: var(--faint);\n}\n.cls__rows {\n display: flex;\n flex-direction: column;\n}\n\n.cls__member {\n display: flex;\n align-items: baseline;\n flex-wrap: wrap;\n gap: 7px;\n width: 100%;\n padding: 5px 16px;\n background: transparent;\n border: none;\n font-family: var(--font-mono);\n font-size: 12.5px;\n text-align: left;\n}\n.cls__member:hover {\n background: var(--surface-2);\n}\n.cls__method--btn {\n cursor: pointer;\n}\n.cls__caret {\n flex-shrink: 0;\n width: 9px;\n margin-left: -16px;\n padding-left: 7px;\n font-size: 8px;\n color: var(--faint);\n align-self: center;\n transition: transform 0.12s;\n}\n.cls__caret--open {\n transform: rotate(90deg);\n}\n.cls__mods {\n color: var(--muted);\n}\n.cls__type {\n color: var(--accent);\n}\n.cls__name {\n color: var(--text-strong);\n font-weight: 500;\n}\n.cls__name--special {\n color: var(--muted);\n font-style: italic;\n font-weight: 400;\n}\n.cls__params {\n display: inline;\n min-width: 0;\n}\n.cls__punc {\n color: var(--faint);\n}\n\n/* Local-variable detail (expanded method). */\n.cls__locals {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 6px 14px;\n margin: 0 16px 6px 30px;\n padding: 7px 12px;\n border-left: 2px solid var(--border-2);\n background: var(--code-bg);\n border-radius: 0 6px 6px 0;\n}\n.cls__locals-label {\n font-size: 10px;\n letter-spacing: 0.06em;\n color: var(--faint);\n text-transform: uppercase;\n}\n.cls__local {\n display: inline-flex;\n align-items: baseline;\n gap: 5px;\n font-family: var(--font-mono);\n font-size: 12px;\n}\n.cls__localname {\n color: var(--text);\n}\n\n.cls__foot {\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 12px 16px 22px;\n color: var(--faint);\n font-size: 11.5px;\n}\n</style>\n"],"mappings":""}
@@ -162,3 +162,5 @@ var ie = { class: "cls" }, _ = {
162
162
  });
163
163
  //#endregion
164
164
  export { K as default };
165
+
166
+ //# sourceMappingURL=ClassViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClassViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/ClassViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport {\n parseClass,\n NotClassFileError,\n type ClassInfo,\n type ClassLocal,\n type ClassMethod,\n} from '@/lib/classfile'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's Java `.class` mode: fetch the bytes, parse the structure entirely in-browser (see\n * lib/classfile.ts), and list the FIELDS and METHODS — a lightweight `javap`. Mirrors BinObjBrowser: the\n * bytes come from the same download endpoint as any other file and never leave the browser. No decompilation\n * (method bodies aren't reconstructed); a method's local variables expand inline when the class carried a\n * LocalVariableTable (`javac -g`).\n */\nconst props = defineProps<{ url: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'error' | 'ready'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst info = ref<ClassInfo | null>(null)\nconst openMethods = ref<Set<number>>(new Set())\n\nasync function load(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n info.value = null\n openMethods.value = new Set()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n info.value = parseClass(await res.arrayBuffer())\n status.value = 'ready'\n } catch (e) {\n errorMsg.value =\n e instanceof NotClassFileError ? e.message : e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\n// Short type for display (drop the package); the full name stays in the title.\nfunction shortType(t: string | null): string {\n if (!t) return ''\n const dot = t.replace(/\\[\\]$/g, '').lastIndexOf('.')\n return dot >= 0 ? t.slice(dot + 1) : t\n}\n// `<init>` / `<clinit>` get friendly labels; everything else is its own name.\nfunction methodLabel(m: ClassMethod): string {\n if (m.name === '<init>') return '«构造器»'\n if (m.name === '<clinit>') return '«静态初始化»'\n return m.name\n}\nfunction realLocals(m: ClassMethod): ClassLocal[] {\n return (m.locals ?? []).filter((l) => l.name !== 'this') // slot 0 of an instance method is `this`\n}\nfunction toggleMethod(i: number): void {\n const s = new Set(openMethods.value)\n if (s.has(i)) s.delete(i)\n else s.add(i)\n openMethods.value = s\n}\n\nconst signature = computed(() => {\n const c = info.value\n if (!c) return ''\n const head = [...c.modifiers, c.kind, c.thisClass ?? '?'].join(' ')\n return head\n})\n\nonMounted(load)\nwatch(() => props.url, load)\nonBeforeUnmount(() => openMethods.value.clear())\n</script>\n\n<template>\n <div class=\"cls\">\n <p v-if=\"status === 'loading'\" class=\"cls__msg\">正在解析 .class…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"cls__msg cls__msg--error\">\n <p>{{ errorMsg }}</p>\n <button type=\"button\" class=\"cls__retry\" @click=\"load\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready' && info\">\n <div class=\"cls__summary\">\n <span class=\"cls__kind\">{{ info.kind.toUpperCase() }}</span>\n <span class=\"cls__ver\">字节码 v{{ info.version }}</span>\n <span>{{ info.javaLabel }}</span>\n <span>{{ info.fields.length }} 字段</span>\n <span>{{ info.methods.length }} 方法</span>\n </div>\n\n <!-- signature card: `modifiers kind ThisClass extends Super implements I, J` -->\n <div class=\"cls__sig\">\n <code class=\"cls__sigline\">\n <span class=\"cls__mod\">{{ signature }}</span>\n <template v-if=\"info.superClass && info.superClass !== 'java.lang.Object'\">\n <span class=\"cls__kw\"> extends </span\n ><span class=\"cls__type\" :title=\"info.superClass\">{{\n shortType(info.superClass)\n }}</span>\n </template>\n <template v-if=\"info.interfaces.length\">\n <span class=\"cls__kw\">\n {{\n info.kind === 'interface' || info.kind === '@interface' ? 'extends' : 'implements'\n }}\n </span>\n <span v-for=\"(it, i) in info.interfaces\" :key=\"it\">\n <span v-if=\"i > 0\" class=\"cls__punc\">, </span\n ><span class=\"cls__type\" :title=\"it\">{{ shortType(it) }}</span>\n </span>\n </template>\n </code>\n </div>\n\n <!-- FIELDS -->\n <section class=\"cls__section\">\n <h3 class=\"cls__h\">字段 · FIELDS</h3>\n <p v-if=\"!info.fields.length\" class=\"cls__empty\">无字段</p>\n <div v-else class=\"cls__rows\">\n <div v-for=\"(f, i) in info.fields\" :key=\"i\" class=\"cls__member\">\n <span v-if=\"f.flags.length\" class=\"cls__mods\">{{ f.flags.join(' ') }}</span>\n <span class=\"cls__type\" :title=\"f.type\">{{ shortType(f.type) }}</span>\n <span class=\"cls__name\">{{ f.name }}</span>\n </div>\n </div>\n </section>\n\n <!-- METHODS -->\n <section class=\"cls__section\">\n <h3 class=\"cls__h\">方法 · METHODS</h3>\n <p v-if=\"!info.methods.length\" class=\"cls__empty\">无方法</p>\n <div v-else class=\"cls__rows\">\n <div v-for=\"(m, i) in info.methods\" :key=\"i\" class=\"cls__methodwrap\">\n <component\n :is=\"realLocals(m).length ? 'button' : 'div'\"\n :type=\"realLocals(m).length ? 'button' : undefined\"\n class=\"cls__member cls__method\"\n :class=\"{ 'cls__method--btn': realLocals(m).length }\"\n :title=\"realLocals(m).length ? '点击查看局部变量' : undefined\"\n @click=\"realLocals(m).length && toggleMethod(i)\"\n >\n <span\n v-if=\"realLocals(m).length\"\n class=\"cls__caret\"\n :class=\"{ 'cls__caret--open': openMethods.has(i) }\"\n >▶</span\n >\n <span v-if=\"m.flags.length\" class=\"cls__mods\">{{ m.flags.join(' ') }}</span>\n <span class=\"cls__type\" :title=\"m.ret\">{{ shortType(m.ret) }}</span>\n <span class=\"cls__name\" :class=\"{ 'cls__name--special': m.name.startsWith('<') }\">{{\n methodLabel(m)\n }}</span>\n <span class=\"cls__params\">\n <span class=\"cls__punc\">(</span\n ><template v-for=\"(pt, pi) in m.params\" :key=\"pi\"\n ><span v-if=\"pi > 0\" class=\"cls__punc\">, </span\n ><span class=\"cls__type\" :title=\"pt\">{{ shortType(pt) }}</span></template\n ><span class=\"cls__punc\">)</span>\n </span>\n </component>\n\n <div v-if=\"openMethods.has(i)\" class=\"cls__locals\">\n <span class=\"cls__locals-label\">局部变量</span>\n <div v-for=\"(l, li) in realLocals(m)\" :key=\"li\" class=\"cls__local\">\n <span class=\"cls__type\" :title=\"l.type\">{{ shortType(l.type) }}</span>\n <span class=\"cls__localname\">{{ l.name }}</span>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <div class=\"cls__foot\">\n <Icon name=\"code\" :size=\"13\" />\n <span>仅解析结构(字段 / 方法),不反编译方法体</span>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.cls {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface);\n}\n\n.cls__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.cls__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.cls__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.cls__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Summary bar (mirrors BinObjBrowser). */\n.cls__summary {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px 12px;\n padding: 9px 16px;\n border-bottom: 1px solid var(--border);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--faint);\n}\n.cls__kind {\n font-weight: 700;\n color: var(--accent);\n}\n.cls__ver {\n font-variant-numeric: tabular-nums;\n}\n\n/* Signature card. */\n.cls__sig {\n flex-shrink: 0;\n padding: 12px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface-2);\n}\n.cls__sigline {\n font-family: var(--font-mono);\n font-size: 13px;\n line-height: 1.6;\n word-break: break-word;\n}\n.cls__mod {\n color: var(--text-strong);\n font-weight: 600;\n}\n.cls__kw {\n color: var(--muted);\n}\n\n.cls__section {\n flex-shrink: 0;\n padding: 12px 0 6px;\n border-bottom: 1px solid var(--border);\n}\n.cls__h {\n margin: 0 0 6px;\n padding: 0 16px;\n font-size: 10.5px;\n font-weight: 700;\n letter-spacing: 0.08em;\n color: var(--faint);\n}\n.cls__empty {\n margin: 0;\n padding: 2px 16px 6px;\n font-size: 12px;\n color: var(--faint);\n}\n.cls__rows {\n display: flex;\n flex-direction: column;\n}\n\n.cls__member {\n display: flex;\n align-items: baseline;\n flex-wrap: wrap;\n gap: 7px;\n width: 100%;\n padding: 5px 16px;\n background: transparent;\n border: none;\n font-family: var(--font-mono);\n font-size: 12.5px;\n text-align: left;\n}\n.cls__member:hover {\n background: var(--surface-2);\n}\n.cls__method--btn {\n cursor: pointer;\n}\n.cls__caret {\n flex-shrink: 0;\n width: 9px;\n margin-left: -16px;\n padding-left: 7px;\n font-size: 8px;\n color: var(--faint);\n align-self: center;\n transition: transform 0.12s;\n}\n.cls__caret--open {\n transform: rotate(90deg);\n}\n.cls__mods {\n color: var(--muted);\n}\n.cls__type {\n color: var(--accent);\n}\n.cls__name {\n color: var(--text-strong);\n font-weight: 500;\n}\n.cls__name--special {\n color: var(--muted);\n font-style: italic;\n font-weight: 400;\n}\n.cls__params {\n display: inline;\n min-width: 0;\n}\n.cls__punc {\n color: var(--faint);\n}\n\n/* Local-variable detail (expanded method). */\n.cls__locals {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: 6px 14px;\n margin: 0 16px 6px 30px;\n padding: 7px 12px;\n border-left: 2px solid var(--border-2);\n background: var(--code-bg);\n border-radius: 0 6px 6px 0;\n}\n.cls__locals-label {\n font-size: 10px;\n letter-spacing: 0.06em;\n color: var(--faint);\n text-transform: uppercase;\n}\n.cls__local {\n display: inline-flex;\n align-items: baseline;\n gap: 5px;\n font-family: var(--font-mono);\n font-size: 12px;\n}\n.cls__localname {\n color: var(--text);\n}\n\n.cls__foot {\n display: flex;\n align-items: center;\n gap: 7px;\n padding: 12px 16px 22px;\n color: var(--faint);\n font-size: 11.5px;\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./CodeBlock.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-71d58741"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=CodeBlock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CodeBlock.js","names":[],"sources":["../../src/components/CodeBlock.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref } from 'vue'\nimport { highlightCode } from '@/lib/highlight'\nimport { codePreview } from '@/lib/codePreview'\nimport { useFileViewer } from '@/composables/useFileViewer'\nimport Icon from '@/components/icons/Icon.vue'\nimport ClampBlock from '@/components/ui/ClampBlock'\n\n/**\n * Fenced code block: header (language label + copy button) over a highlighted <pre>.\n * Without `maxHeight` a tall block clamps to a Show more / less toggle; pass\n * `maxHeight` (e.g. timeline tool steps) to keep the legacy fixed-height scroll.\n */\nconst props = withDefaults(\n defineProps<{\n code: string\n lang?: string\n maxHeight?: string\n margin?: string\n }>(),\n { lang: '', margin: '4px 0 20px' },\n)\n\nconst label = computed(() => (props.lang || '').trim().toLowerCase())\nconst html = computed(() => highlightCode(props.code, label.value))\n\n// html/svg/markdown fences can be \"run\": a Run button opens the side viewer (FileViewerPanel) with the\n// code as inline content — render shows the live page/picture/document, no fetch. null → no Run button.\nconst preview = computed(() => codePreview(label.value))\nconst { open } = useFileViewer()\nfunction run(): void {\n const p = preview.value\n if (!p) return\n open({ name: `代码片段.${p.ext}`, content: String(props.code), ...p.fields })\n}\n\nconst preStyle = computed(() =>\n props.maxHeight ? { maxHeight: props.maxHeight, overflowY: 'auto' as const } : {},\n)\n\nconst copied = ref(false)\nlet timer: ReturnType<typeof setTimeout> | undefined\nasync function copy() {\n try {\n await navigator.clipboard?.writeText(String(props.code))\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(timer)\n timer = setTimeout(() => (copied.value = false), 1400)\n}\nonBeforeUnmount(() => clearTimeout(timer))\n</script>\n\n<template>\n <div class=\"code-block\" :style=\"{ margin }\">\n <div class=\"code-block__head\">\n <span class=\"code-block__lang\">{{ label || 'text' }}</span>\n <div class=\"code-block__actions\">\n <button\n v-if=\"preview\"\n class=\"code-block__btn\"\n type=\"button\"\n title=\"在侧栏预览运行\"\n @click=\"run\"\n >\n <Icon name=\"play\" :size=\"13\" />\n <span>Run</span>\n </button>\n <button class=\"code-block__btn\" type=\"button\" @click=\"copy\">\n <Icon name=\"copy\" :size=\"13\" />\n <span>{{ copied ? 'Copied' : 'Copy' }}</span>\n </button>\n </div>\n </div>\n <ClampBlock v-if=\"!maxHeight\" :max-height=\"340\" fade=\"var(--code-bg)\" :inset=\"16\">\n <pre\n class=\"code-block__pre code-block__pre--clamped\"\n ><code v-if=\"html\" class=\"hljs\" v-html=\"html\" /><code v-else>{{ code }}</code></pre>\n </ClampBlock>\n <pre\n v-else\n class=\"code-block__pre\"\n :style=\"preStyle\"\n ><code v-if=\"html\" class=\"hljs\" v-html=\"html\" /><code v-else>{{ code }}</code></pre>\n </div>\n</template>\n\n<style scoped>\n.code-block {\n border-radius: 12px;\n overflow: hidden;\n border: 1px solid var(--code-border);\n background: var(--code-bg);\n box-shadow: var(--shadow);\n}\n.code-block__head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 7px 10px 7px 14px;\n background: var(--code-head);\n border-bottom: 1px solid var(--code-border);\n}\n.code-block__lang {\n font-family: var(--font-mono);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.code-block__actions {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n}\n.code-block__btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n background: transparent;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n color: var(--faint);\n font-family: inherit;\n font-size: 12px;\n padding: 3px 6px;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.code-block__btn:hover {\n background: var(--surface-2);\n color: var(--muted);\n}\n.code-block__pre {\n margin: 0;\n padding: 14px 16px;\n overflow-x: auto;\n font-family: var(--font-mono);\n font-size: 13.5px;\n line-height: 1.7;\n color: var(--text);\n white-space: pre;\n tab-size: 2;\n}\n.code-block__pre code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n/* Show more/less path: hide the horizontal scrollbar (content still scrolls via\n * wheel/trackpad) so the clamp fade + toggle read cleanly. */\n.code-block__pre--clamped {\n scrollbar-width: none; /* Firefox */\n}\n.code-block__pre--clamped::-webkit-scrollbar {\n display: none; /* WebKit/Blink */\n}\n</style>\n"],"mappings":""}
@@ -77,3 +77,5 @@ var y = { class: "code-block__head" }, b = { class: "code-block__lang" }, x = {
77
77
  });
78
78
  //#endregion
79
79
  export { D as default };
80
+
81
+ //# sourceMappingURL=CodeBlock.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CodeBlock.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/CodeBlock.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref } from 'vue'\nimport { highlightCode } from '@/lib/highlight'\nimport { codePreview } from '@/lib/codePreview'\nimport { useFileViewer } from '@/composables/useFileViewer'\nimport Icon from '@/components/icons/Icon.vue'\nimport ClampBlock from '@/components/ui/ClampBlock'\n\n/**\n * Fenced code block: header (language label + copy button) over a highlighted <pre>.\n * Without `maxHeight` a tall block clamps to a Show more / less toggle; pass\n * `maxHeight` (e.g. timeline tool steps) to keep the legacy fixed-height scroll.\n */\nconst props = withDefaults(\n defineProps<{\n code: string\n lang?: string\n maxHeight?: string\n margin?: string\n }>(),\n { lang: '', margin: '4px 0 20px' },\n)\n\nconst label = computed(() => (props.lang || '').trim().toLowerCase())\nconst html = computed(() => highlightCode(props.code, label.value))\n\n// html/svg/markdown fences can be \"run\": a Run button opens the side viewer (FileViewerPanel) with the\n// code as inline content — render shows the live page/picture/document, no fetch. null → no Run button.\nconst preview = computed(() => codePreview(label.value))\nconst { open } = useFileViewer()\nfunction run(): void {\n const p = preview.value\n if (!p) return\n open({ name: `代码片段.${p.ext}`, content: String(props.code), ...p.fields })\n}\n\nconst preStyle = computed(() =>\n props.maxHeight ? { maxHeight: props.maxHeight, overflowY: 'auto' as const } : {},\n)\n\nconst copied = ref(false)\nlet timer: ReturnType<typeof setTimeout> | undefined\nasync function copy() {\n try {\n await navigator.clipboard?.writeText(String(props.code))\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(timer)\n timer = setTimeout(() => (copied.value = false), 1400)\n}\nonBeforeUnmount(() => clearTimeout(timer))\n</script>\n\n<template>\n <div class=\"code-block\" :style=\"{ margin }\">\n <div class=\"code-block__head\">\n <span class=\"code-block__lang\">{{ label || 'text' }}</span>\n <div class=\"code-block__actions\">\n <button\n v-if=\"preview\"\n class=\"code-block__btn\"\n type=\"button\"\n title=\"在侧栏预览运行\"\n @click=\"run\"\n >\n <Icon name=\"play\" :size=\"13\" />\n <span>Run</span>\n </button>\n <button class=\"code-block__btn\" type=\"button\" @click=\"copy\">\n <Icon name=\"copy\" :size=\"13\" />\n <span>{{ copied ? 'Copied' : 'Copy' }}</span>\n </button>\n </div>\n </div>\n <ClampBlock v-if=\"!maxHeight\" :max-height=\"340\" fade=\"var(--code-bg)\" :inset=\"16\">\n <pre\n class=\"code-block__pre code-block__pre--clamped\"\n ><code v-if=\"html\" class=\"hljs\" v-html=\"html\" /><code v-else>{{ code }}</code></pre>\n </ClampBlock>\n <pre\n v-else\n class=\"code-block__pre\"\n :style=\"preStyle\"\n ><code v-if=\"html\" class=\"hljs\" v-html=\"html\" /><code v-else>{{ code }}</code></pre>\n </div>\n</template>\n\n<style scoped>\n.code-block {\n border-radius: 12px;\n overflow: hidden;\n border: 1px solid var(--code-border);\n background: var(--code-bg);\n box-shadow: var(--shadow);\n}\n.code-block__head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 7px 10px 7px 14px;\n background: var(--code-head);\n border-bottom: 1px solid var(--code-border);\n}\n.code-block__lang {\n font-family: var(--font-mono);\n font-size: 11.5px;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.code-block__actions {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n}\n.code-block__btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n background: transparent;\n border: none;\n border-radius: 6px;\n cursor: pointer;\n color: var(--faint);\n font-family: inherit;\n font-size: 12px;\n padding: 3px 6px;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.code-block__btn:hover {\n background: var(--surface-2);\n color: var(--muted);\n}\n.code-block__pre {\n margin: 0;\n padding: 14px 16px;\n overflow-x: auto;\n font-family: var(--font-mono);\n font-size: 13.5px;\n line-height: 1.7;\n color: var(--text);\n white-space: pre;\n tab-size: 2;\n}\n.code-block__pre code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n/* Show more/less path: hide the horizontal scrollbar (content still scrolls via\n * wheel/trackpad) so the clamp fade + toggle read cleanly. */\n.code-block__pre--clamped {\n scrollbar-width: none; /* Firefox */\n}\n.code-block__pre--clamped::-webkit-scrollbar {\n display: none; /* WebKit/Blink */\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;EAaA,IAAM,IAAQ,GAUR,IAAQ,SAAgB,EAAM,QAAQ,GAAA,CAAI,KAAK,CAAC,CAAC,YAAY,CAAC,GAC9D,IAAO,QAAe,EAAc,EAAM,MAAM,EAAM,KAAK,CAAC,GAI5D,IAAU,QAAe,EAAY,EAAM,KAAK,CAAC,GACjD,EAAE,YAAS,EAAc;EAC/B,SAAS,IAAY;GACnB,IAAM,IAAI,EAAQ;GACb,KACL,EAAK;IAAE,MAAM,QAAQ,EAAE;IAAO,SAAS,OAAO,EAAM,IAAI;IAAG,GAAG,EAAE;GAAO,CAAC;EAC1E;EAEA,IAAM,IAAW,QACf,EAAM,YAAY;GAAE,WAAW,EAAM;GAAW,WAAW;EAAgB,IAAI,CAAC,CAClF,GAEM,IAAS,EAAI,EAAK,GACpB;EACJ,eAAe,IAAO;GACpB,IAAI;IACF,MAAM,UAAU,WAAW,UAAU,OAAO,EAAM,IAAI,CAAC;GACzD,QAAQ,CAER;GAGA,AAFA,EAAO,QAAQ,IACf,aAAa,CAAK,GAClB,IAAQ,iBAAkB,EAAO,QAAQ,IAAQ,IAAI;EACvD;SACA,QAAsB,aAAa,CAAK,CAAC,mBAIvC,EA8BM,OAAA;GA9BD,OAAM;GAAc,OAAK,EAAA,EAAA,QAAI,EAAA,OAAM,CAAA;MACtC,EAkBM,OAlBN,GAkBM,CAjBJ,EAA2D,QAA3D,GAA2D,EAAzB,EAAA,SAAK,MAAA,GAAA,CAAA,GACvC,EAeM,OAfN,GAeM,CAbI,EAAA,SAAA,EAAA,GADR,EASS,UAAA;;GAPP,OAAM;GACN,MAAK;GACL,OAAM;GACL,SAAO;MAER,EAA+B,GAAA;GAAzB,MAAK;GAAQ,MAAM;eACzB,EAAgB,QAAA,MAAV,OAAG,EAAA,CAAA,CAAA,KAAA,EAAA,IAAA,EAAA,GAEX,EAGS,UAAA;GAHD,OAAM;GAAkB,MAAK;GAAU,SAAO;MACpD,EAA+B,GAAA;GAAzB,MAAK;GAAQ,MAAM;MACzB,EAA6C,QAAA,MAAA,EAApC,EAAA,QAAM,WAAA,MAAA,GAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,CAAA,GAIF,EAAA,kBAKnB,EAIoF,OAAA;;GAFlF,OAAM;GACL,OAAK,EAAE,EAAA,KAAQ;MACL,EAAA,SAAA,EAAA,GAAZ,EAA+C,QAAA;;GAA7B,OAAM;GAAO,WAAQ,EAAA;0BAAQ,EAA8B,QAAA,GAAA,EAAd,EAAA,IAAI,GAAA,CAAA,EAAA,GAAA,CAAA,MATjD,EAAA,GAAnB,EAIa,EAAA,CAAA,GAAA;;GAJkB,cAAY;GAAK,MAAK;GAAkB,OAAO;;oBAGQ,CAFpF,EAEoF,OAFpF,GAEoF,CAAvE,EAAA,SAAA,EAAA,GAAZ,EAA+C,QAAA;;IAA7B,OAAM;IAAO,WAAQ,EAAA;2BAAQ,EAA8B,QAAA,GAAA,EAAd,EAAA,IAAI,GAAA,CAAA,EAAA,CAAA,CAAA,CAAA;;KAMF,GAAA,CAAA"}
@@ -5,3 +5,5 @@ import t from "./CodeView.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-f432e526"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=CodeView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CodeView.js","names":[],"sources":["../../src/components/CodeView.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'\nimport { escapeHtml, highlightCode, highlightLines } from '@/lib/highlight'\nimport { computeFoldRegions } from '@/lib/codefold'\nimport { canFormat, formatCode } from '@/lib/codeformat'\nimport { fileExt } from '@/lib/fileType'\nimport { langLabel, searchLangs } from '@/lib/highlightLangs'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's source-code view: a line-number gutter + syntax-highlighted code, with indentation-based\n * code FOLDING (click a chevron to collapse a block; see lib/codefold.ts), on-demand Prettier FORMATTING (the\n * 格式化 button, shown only for languages Prettier can parse — see lib/codeformat.ts; loaded lazily), and a\n * manual HIGHLIGHT-LANGUAGE picker (the 语言 button — auto-detection by extension/mime is often empty or wrong,\n * so any file can be re-colorized as JSON / XML / Python / … or forced to plain text; see lib/highlightLangs).\n * Used for every non-rendered file (plain text, code, or markdown/html/svg viewed as source). Highlighting is\n * split per line (see highlightLines) so rows can be hidden individually.\n *\n * Very large files (> {@link MAX_FOLD_LINES}) skip the per-line DOM and fall back to a single highlighted block\n * — folding a 10k-line file isn't worth tens of thousands of row elements, and such files mostly reach the\n * streaming log viewer anyway (see isLogFile).\n */\nconst props = defineProps<{ code: string; lang: string; name?: string }>()\n\nconst MAX_FOLD_LINES = 4000\n\n// Manual highlight-language override (null = use the auto-detected `props.lang`; '' = forced plain text). The\n// effective language feeds highlighting AND the 格式化 button (so \"force JSON, then format\" works). Reset when a\n// different file / detected language arrives (see the reset watch below).\nconst langOverride = ref<string | null>(null)\nconst effLang = computed(() => langOverride.value ?? props.lang)\n\n// Formatted output (null = showing the original). The view always renders `displayCode`.\nconst formatted = ref<string | null>(null)\nconst formatting = ref(false)\nconst formatErr = ref(false)\nconst displayCode = computed(() => formatted.value ?? props.code)\n\nconst ext = computed(() => fileExt(props.name ?? ''))\nconst canFmt = computed(() => canFormat(ext.value, effLang.value))\n\nconst rawLines = computed(() => displayCode.value.split('\\n'))\nconst lineCount = computed(() => rawLines.value.length)\nconst foldable = computed(() => lineCount.value <= MAX_FOLD_LINES)\n\nconst lineHtml = computed(() => highlightLines(displayCode.value, effLang.value))\nconst regions = computed(() => computeFoldRegions(displayCode.value))\nconst folded = reactive(new Set<number>())\n\n// A new file (or a re-detected language) resets the formatting toggle AND the manual language override; any\n// change to the shown code clears stale fold state.\nwatch(\n () => [props.code, props.lang],\n () => {\n formatted.value = null\n formatErr.value = false\n langOverride.value = null\n },\n)\nwatch(displayCode, () => folded.clear())\n\n// ── highlight-language picker ────────────────────────────────────────────────────────────────────\nconst langMenuOpen = ref(false)\nconst langMenuRef = ref<HTMLElement | null>(null)\nconst langQuery = ref('')\nconst langResults = computed(() => searchLangs(langQuery.value))\n\nfunction chooseLang(id: string): void {\n langOverride.value = id\n langMenuOpen.value = false\n langQuery.value = ''\n}\n\n// Close the menu on an outside click or Esc — listeners live only while it's open (SSR-guarded). The toggle\n// button + popup both sit inside langMenuRef, so a click on either is \"inside\" and never self-closes.\nfunction onLangDocPointer(e: MouseEvent): void {\n if (langMenuRef.value && !langMenuRef.value.contains(e.target as Node)) langMenuOpen.value = false\n}\nfunction onLangKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') langMenuOpen.value = false\n}\nwatch(langMenuOpen, (on) => {\n if (typeof document === 'undefined') return\n if (on) {\n document.addEventListener('click', onLangDocPointer)\n document.addEventListener('keydown', onLangKey)\n } else {\n document.removeEventListener('click', onLangDocPointer)\n document.removeEventListener('keydown', onLangKey)\n }\n})\nonBeforeUnmount(() => {\n if (typeof document === 'undefined') return\n document.removeEventListener('click', onLangDocPointer)\n document.removeEventListener('keydown', onLangKey)\n})\n\nlet errTimer: ReturnType<typeof setTimeout> | undefined\nasync function toggleFormat(): Promise<void> {\n if (formatted.value != null) {\n formatted.value = null // revert to the original\n return\n }\n if (formatting.value) return\n formatting.value = true\n formatErr.value = false\n try {\n formatted.value = await formatCode(props.code, { ext: ext.value, lang: effLang.value })\n } catch {\n formatErr.value = true\n clearTimeout(errTimer)\n errTimer = setTimeout(() => (formatErr.value = false), 2200)\n } finally {\n formatting.value = false\n }\n}\n\n// Visible line indices, skipping the bodies of folded regions (and any nested folds within them).\nconst visible = computed(() => {\n const out: number[] = []\n const n = lineCount.value\n let i = 0\n while (i < n) {\n out.push(i)\n const end = folded.has(i) ? regions.value.get(i) : undefined\n i = end != null ? end + 1 : i + 1\n }\n return out\n})\n\nfunction toggle(i: number): void {\n if (folded.has(i)) folded.delete(i)\n else folded.add(i)\n}\n\n// Blob fallback for huge files: one highlighted block + a newline-joined gutter.\nconst blobHtml = computed(() =>\n effLang.value ? highlightCode(displayCode.value, effLang.value) : escapeHtml(displayCode.value),\n)\nconst blobGutter = computed(() =>\n Array.from({ length: lineCount.value }, (_, i) => i + 1).join('\\n'),\n)\n</script>\n\n<template>\n <div class=\"cv\">\n <!-- floating tools, top-right over the code: highlight-language picker + Prettier format toggle -->\n <div class=\"cv__tools\">\n <!-- highlight-language picker: force which grammar colorizes the source (auto-detect is often empty/wrong) -->\n <div ref=\"langMenuRef\" class=\"cv__lang\">\n <button\n type=\"button\"\n class=\"cv__toolbtn\"\n :class=\"{ 'cv__toolbtn--on': langMenuOpen }\"\n title=\"高亮语言 —— 选择用哪种语言着色(自动识别不准时手动指定)\"\n :aria-haspopup=\"true\"\n :aria-expanded=\"langMenuOpen\"\n @click=\"langMenuOpen = !langMenuOpen\"\n >\n <Icon name=\"code\" :size=\"12\" /><span>{{ langLabel(effLang) }}</span>\n </button>\n <div v-if=\"langMenuOpen\" class=\"cv__langpop\" role=\"menu\">\n <input\n v-model=\"langQuery\"\n class=\"cv__langsearch\"\n type=\"text\"\n placeholder=\"搜索语言…\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n />\n <div class=\"cv__langlist\">\n <button\n v-if=\"!langQuery\"\n type=\"button\"\n class=\"cv__langitem\"\n :class=\"{ 'cv__langitem--on': effLang === '' }\"\n role=\"menuitemradio\"\n :aria-checked=\"effLang === ''\"\n @click=\"chooseLang('')\"\n >\n <span class=\"cv__langitem-label\">纯文本(无高亮)</span>\n <Icon v-if=\"effLang === ''\" name=\"check\" :size=\"14\" class=\"cv__langitem-check\" />\n </button>\n <button\n v-for=\"l in langResults\"\n :key=\"l.id\"\n type=\"button\"\n class=\"cv__langitem\"\n :class=\"{ 'cv__langitem--on': effLang === l.id }\"\n role=\"menuitemradio\"\n :aria-checked=\"effLang === l.id\"\n @click=\"chooseLang(l.id)\"\n >\n <span class=\"cv__langitem-label\">{{ l.label }}</span>\n <Icon v-if=\"effLang === l.id\" name=\"check\" :size=\"14\" class=\"cv__langitem-check\" />\n </button>\n <p v-if=\"langQuery && !langResults.length\" class=\"cv__langempty\">无匹配语言</p>\n </div>\n </div>\n </div>\n\n <!-- format toggle (only for languages Prettier can parse) -->\n <button\n v-if=\"canFmt\"\n type=\"button\"\n class=\"cv__toolbtn cv__fmt\"\n :class=\"{ 'cv__toolbtn--on': formatted != null, 'cv__fmt--err': formatErr }\"\n :disabled=\"formatting\"\n :title=\"\n formatErr\n ? '格式化失败:源码可能无法解析'\n : formatted != null\n ? '还原为原始格式'\n : '用 Prettier 格式化'\n \"\n @click=\"toggleFormat\"\n >\n {{\n formatErr\n ? '格式化失败'\n : formatting\n ? '格式化中…'\n : formatted != null\n ? '还原'\n : '格式化'\n }}\n </button>\n </div>\n\n <!-- huge file: single highlighted block (no folding) -->\n <div v-if=\"!foldable\" class=\"cv__scroll cv__scroll--blob\">\n <div class=\"cv__codewrap\">\n <pre class=\"cv__gutter\" aria-hidden=\"true\">{{ blobGutter }}</pre>\n <div class=\"cv__codecol\">\n <pre class=\"cv__pre\"><code class=\"hljs\" v-html=\"blobHtml\" /></pre>\n </div>\n </div>\n </div>\n\n <!-- foldable: one row per visible line, with a fold chevron on block headers -->\n <div v-else class=\"cv__scroll\">\n <div class=\"cv__rows\">\n <div v-for=\"i in visible\" :key=\"i\" class=\"cv__row\">\n <span class=\"cv__gut\">\n <button\n v-if=\"regions.has(i)\"\n type=\"button\"\n class=\"cv__fold\"\n :title=\"folded.has(i) ? '展开' : '折叠'\"\n @click=\"toggle(i)\"\n >\n <Icon name=\"chevronRight\" :size=\"12\" :class=\"{ 'cv__fold--open': !folded.has(i) }\" />\n </button>\n <span v-else class=\"cv__fold cv__fold--empty\" />\n <span class=\"cv__ln\">{{ i + 1 }}</span>\n </span>\n <code class=\"cv__code hljs\" v-html=\"lineHtml[i]\" />\n <button\n v-if=\"folded.has(i)\"\n type=\"button\"\n class=\"cv__ellipsis\"\n :title=\"`展开折叠的 ${(regions.get(i) ?? i) - i} 行`\"\n @click=\"toggle(i)\"\n >\n ⋯\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.cv {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Floating tools bar — top-right, above the code (highlight-language picker + format toggle). */\n.cv__tools {\n position: absolute;\n top: 8px;\n right: 14px;\n z-index: 3;\n display: flex;\n align-items: flex-start;\n gap: 6px;\n}\n\n/* Shared small pill used by both floating tools. */\n.cv__toolbtn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n max-width: 190px;\n padding: 3px 10px;\n font-family: inherit;\n font-size: 11.5px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n box-shadow: var(--shadow);\n transition:\n background 0.15s,\n color 0.15s,\n border-color 0.15s;\n}\n.cv__toolbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.cv__toolbtn span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cv__toolbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.cv__toolbtn--on:hover {\n color: #fff;\n}\n\n/* Format toggle specifics (disabled + error states). */\n.cv__fmt:disabled {\n cursor: default;\n opacity: 0.7;\n}\n.cv__fmt--err {\n color: var(--danger);\n border-color: var(--danger);\n}\n\n/* Highlight-language picker dropdown (anchored under its pill, right-aligned). */\n.cv__lang {\n position: relative;\n display: inline-flex;\n}\n.cv__langpop {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 4;\n width: 224px;\n padding: 7px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 10px;\n box-shadow: var(--shadow);\n}\n.cv__langsearch {\n width: 100%;\n box-sizing: border-box;\n padding: 6px 9px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n outline: none;\n}\n.cv__langsearch:focus {\n border-color: var(--accent);\n}\n.cv__langlist {\n max-height: 288px;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: 6px;\n}\n.cv__langitem {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 8px;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--text);\n font-family: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n transition:\n background 0.12s,\n color 0.12s;\n}\n.cv__langitem:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.cv__langitem--on {\n color: var(--accent);\n}\n.cv__langitem-label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cv__langitem-check {\n flex-shrink: 0;\n color: var(--accent);\n}\n.cv__langempty {\n margin: 0;\n padding: 10px 8px;\n font-size: 12px;\n color: var(--faint);\n text-align: center;\n}\n\n.cv__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n\n/* Foldable per-line view -------------------------------------------------------------------------- */\n.cv__rows {\n min-width: max-content;\n padding: 14px 0;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n}\n.cv__row {\n display: flex;\n align-items: flex-start;\n}\n/* Gutter (fold chevron + line number) sticks to the left so it stays put while long lines scroll. */\n.cv__gut {\n position: sticky;\n left: 0;\n z-index: 1;\n flex-shrink: 0;\n display: flex;\n align-items: flex-start;\n background: var(--code-bg);\n user-select: none;\n}\n.cv__fold {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 16px;\n height: var(--fv-code-lh, 21px);\n padding: 0;\n border: none;\n background: transparent;\n color: var(--faint);\n cursor: pointer;\n}\n.cv__fold--empty {\n cursor: default;\n}\n.cv__fold :deep(svg) {\n transition: transform 0.12s;\n}\n.cv__fold--open {\n transform: rotate(90deg);\n}\n.cv__fold:hover :deep(svg) {\n color: var(--accent);\n}\n.cv__ln {\n flex-shrink: 0;\n width: 48px;\n padding: 0 12px 0 4px;\n text-align: right;\n color: var(--faint);\n}\n.cv__code {\n flex: 1 1 auto;\n padding: 0 18px 0 14px;\n white-space: pre;\n color: var(--text);\n background: transparent;\n border: none;\n font-family: inherit;\n tab-size: 2;\n}\n.cv__ellipsis {\n align-self: center;\n margin: 0 0 0 2px;\n padding: 0 7px;\n font-family: inherit;\n line-height: 1.3;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 5px;\n cursor: pointer;\n}\n.cv__ellipsis:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Huge-file blob fallback (mirrors the panel's original source layout) ----------------------------- */\n.cv__scroll--blob {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.cv__codewrap {\n display: flex;\n align-items: stretch;\n min-height: 100%;\n padding: 14px 0;\n}\n.cv__gutter {\n flex-shrink: 0;\n margin: 0;\n padding: 0 12px 0 16px;\n border-right: 1px solid var(--code-border);\n text-align: right;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n color: var(--faint);\n user-select: none;\n white-space: pre;\n}\n.cv__codecol {\n flex: 1 1 auto;\n min-width: 0;\n overflow-x: auto;\n}\n.cv__pre {\n margin: 0;\n padding: 0 18px 0 14px;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n color: var(--text);\n white-space: pre;\n tab-size: 2;\n}\n.cv__pre code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n</style>\n"],"mappings":""}
@@ -178,3 +178,5 @@ var le = { class: "cv" }, ue = { class: "cv__tools" }, de = ["aria-expanded"], f
178
178
  });
179
179
  //#endregion
180
180
  export { P as default };
181
+
182
+ //# sourceMappingURL=CodeView.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CodeView.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/CodeView.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'\nimport { escapeHtml, highlightCode, highlightLines } from '@/lib/highlight'\nimport { computeFoldRegions } from '@/lib/codefold'\nimport { canFormat, formatCode } from '@/lib/codeformat'\nimport { fileExt } from '@/lib/fileType'\nimport { langLabel, searchLangs } from '@/lib/highlightLangs'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's source-code view: a line-number gutter + syntax-highlighted code, with indentation-based\n * code FOLDING (click a chevron to collapse a block; see lib/codefold.ts), on-demand Prettier FORMATTING (the\n * 格式化 button, shown only for languages Prettier can parse — see lib/codeformat.ts; loaded lazily), and a\n * manual HIGHLIGHT-LANGUAGE picker (the 语言 button — auto-detection by extension/mime is often empty or wrong,\n * so any file can be re-colorized as JSON / XML / Python / … or forced to plain text; see lib/highlightLangs).\n * Used for every non-rendered file (plain text, code, or markdown/html/svg viewed as source). Highlighting is\n * split per line (see highlightLines) so rows can be hidden individually.\n *\n * Very large files (> {@link MAX_FOLD_LINES}) skip the per-line DOM and fall back to a single highlighted block\n * — folding a 10k-line file isn't worth tens of thousands of row elements, and such files mostly reach the\n * streaming log viewer anyway (see isLogFile).\n */\nconst props = defineProps<{ code: string; lang: string; name?: string }>()\n\nconst MAX_FOLD_LINES = 4000\n\n// Manual highlight-language override (null = use the auto-detected `props.lang`; '' = forced plain text). The\n// effective language feeds highlighting AND the 格式化 button (so \"force JSON, then format\" works). Reset when a\n// different file / detected language arrives (see the reset watch below).\nconst langOverride = ref<string | null>(null)\nconst effLang = computed(() => langOverride.value ?? props.lang)\n\n// Formatted output (null = showing the original). The view always renders `displayCode`.\nconst formatted = ref<string | null>(null)\nconst formatting = ref(false)\nconst formatErr = ref(false)\nconst displayCode = computed(() => formatted.value ?? props.code)\n\nconst ext = computed(() => fileExt(props.name ?? ''))\nconst canFmt = computed(() => canFormat(ext.value, effLang.value))\n\nconst rawLines = computed(() => displayCode.value.split('\\n'))\nconst lineCount = computed(() => rawLines.value.length)\nconst foldable = computed(() => lineCount.value <= MAX_FOLD_LINES)\n\nconst lineHtml = computed(() => highlightLines(displayCode.value, effLang.value))\nconst regions = computed(() => computeFoldRegions(displayCode.value))\nconst folded = reactive(new Set<number>())\n\n// A new file (or a re-detected language) resets the formatting toggle AND the manual language override; any\n// change to the shown code clears stale fold state.\nwatch(\n () => [props.code, props.lang],\n () => {\n formatted.value = null\n formatErr.value = false\n langOverride.value = null\n },\n)\nwatch(displayCode, () => folded.clear())\n\n// ── highlight-language picker ────────────────────────────────────────────────────────────────────\nconst langMenuOpen = ref(false)\nconst langMenuRef = ref<HTMLElement | null>(null)\nconst langQuery = ref('')\nconst langResults = computed(() => searchLangs(langQuery.value))\n\nfunction chooseLang(id: string): void {\n langOverride.value = id\n langMenuOpen.value = false\n langQuery.value = ''\n}\n\n// Close the menu on an outside click or Esc — listeners live only while it's open (SSR-guarded). The toggle\n// button + popup both sit inside langMenuRef, so a click on either is \"inside\" and never self-closes.\nfunction onLangDocPointer(e: MouseEvent): void {\n if (langMenuRef.value && !langMenuRef.value.contains(e.target as Node)) langMenuOpen.value = false\n}\nfunction onLangKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') langMenuOpen.value = false\n}\nwatch(langMenuOpen, (on) => {\n if (typeof document === 'undefined') return\n if (on) {\n document.addEventListener('click', onLangDocPointer)\n document.addEventListener('keydown', onLangKey)\n } else {\n document.removeEventListener('click', onLangDocPointer)\n document.removeEventListener('keydown', onLangKey)\n }\n})\nonBeforeUnmount(() => {\n if (typeof document === 'undefined') return\n document.removeEventListener('click', onLangDocPointer)\n document.removeEventListener('keydown', onLangKey)\n})\n\nlet errTimer: ReturnType<typeof setTimeout> | undefined\nasync function toggleFormat(): Promise<void> {\n if (formatted.value != null) {\n formatted.value = null // revert to the original\n return\n }\n if (formatting.value) return\n formatting.value = true\n formatErr.value = false\n try {\n formatted.value = await formatCode(props.code, { ext: ext.value, lang: effLang.value })\n } catch {\n formatErr.value = true\n clearTimeout(errTimer)\n errTimer = setTimeout(() => (formatErr.value = false), 2200)\n } finally {\n formatting.value = false\n }\n}\n\n// Visible line indices, skipping the bodies of folded regions (and any nested folds within them).\nconst visible = computed(() => {\n const out: number[] = []\n const n = lineCount.value\n let i = 0\n while (i < n) {\n out.push(i)\n const end = folded.has(i) ? regions.value.get(i) : undefined\n i = end != null ? end + 1 : i + 1\n }\n return out\n})\n\nfunction toggle(i: number): void {\n if (folded.has(i)) folded.delete(i)\n else folded.add(i)\n}\n\n// Blob fallback for huge files: one highlighted block + a newline-joined gutter.\nconst blobHtml = computed(() =>\n effLang.value ? highlightCode(displayCode.value, effLang.value) : escapeHtml(displayCode.value),\n)\nconst blobGutter = computed(() =>\n Array.from({ length: lineCount.value }, (_, i) => i + 1).join('\\n'),\n)\n</script>\n\n<template>\n <div class=\"cv\">\n <!-- floating tools, top-right over the code: highlight-language picker + Prettier format toggle -->\n <div class=\"cv__tools\">\n <!-- highlight-language picker: force which grammar colorizes the source (auto-detect is often empty/wrong) -->\n <div ref=\"langMenuRef\" class=\"cv__lang\">\n <button\n type=\"button\"\n class=\"cv__toolbtn\"\n :class=\"{ 'cv__toolbtn--on': langMenuOpen }\"\n title=\"高亮语言 —— 选择用哪种语言着色(自动识别不准时手动指定)\"\n :aria-haspopup=\"true\"\n :aria-expanded=\"langMenuOpen\"\n @click=\"langMenuOpen = !langMenuOpen\"\n >\n <Icon name=\"code\" :size=\"12\" /><span>{{ langLabel(effLang) }}</span>\n </button>\n <div v-if=\"langMenuOpen\" class=\"cv__langpop\" role=\"menu\">\n <input\n v-model=\"langQuery\"\n class=\"cv__langsearch\"\n type=\"text\"\n placeholder=\"搜索语言…\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n />\n <div class=\"cv__langlist\">\n <button\n v-if=\"!langQuery\"\n type=\"button\"\n class=\"cv__langitem\"\n :class=\"{ 'cv__langitem--on': effLang === '' }\"\n role=\"menuitemradio\"\n :aria-checked=\"effLang === ''\"\n @click=\"chooseLang('')\"\n >\n <span class=\"cv__langitem-label\">纯文本(无高亮)</span>\n <Icon v-if=\"effLang === ''\" name=\"check\" :size=\"14\" class=\"cv__langitem-check\" />\n </button>\n <button\n v-for=\"l in langResults\"\n :key=\"l.id\"\n type=\"button\"\n class=\"cv__langitem\"\n :class=\"{ 'cv__langitem--on': effLang === l.id }\"\n role=\"menuitemradio\"\n :aria-checked=\"effLang === l.id\"\n @click=\"chooseLang(l.id)\"\n >\n <span class=\"cv__langitem-label\">{{ l.label }}</span>\n <Icon v-if=\"effLang === l.id\" name=\"check\" :size=\"14\" class=\"cv__langitem-check\" />\n </button>\n <p v-if=\"langQuery && !langResults.length\" class=\"cv__langempty\">无匹配语言</p>\n </div>\n </div>\n </div>\n\n <!-- format toggle (only for languages Prettier can parse) -->\n <button\n v-if=\"canFmt\"\n type=\"button\"\n class=\"cv__toolbtn cv__fmt\"\n :class=\"{ 'cv__toolbtn--on': formatted != null, 'cv__fmt--err': formatErr }\"\n :disabled=\"formatting\"\n :title=\"\n formatErr\n ? '格式化失败:源码可能无法解析'\n : formatted != null\n ? '还原为原始格式'\n : '用 Prettier 格式化'\n \"\n @click=\"toggleFormat\"\n >\n {{\n formatErr\n ? '格式化失败'\n : formatting\n ? '格式化中…'\n : formatted != null\n ? '还原'\n : '格式化'\n }}\n </button>\n </div>\n\n <!-- huge file: single highlighted block (no folding) -->\n <div v-if=\"!foldable\" class=\"cv__scroll cv__scroll--blob\">\n <div class=\"cv__codewrap\">\n <pre class=\"cv__gutter\" aria-hidden=\"true\">{{ blobGutter }}</pre>\n <div class=\"cv__codecol\">\n <pre class=\"cv__pre\"><code class=\"hljs\" v-html=\"blobHtml\" /></pre>\n </div>\n </div>\n </div>\n\n <!-- foldable: one row per visible line, with a fold chevron on block headers -->\n <div v-else class=\"cv__scroll\">\n <div class=\"cv__rows\">\n <div v-for=\"i in visible\" :key=\"i\" class=\"cv__row\">\n <span class=\"cv__gut\">\n <button\n v-if=\"regions.has(i)\"\n type=\"button\"\n class=\"cv__fold\"\n :title=\"folded.has(i) ? '展开' : '折叠'\"\n @click=\"toggle(i)\"\n >\n <Icon name=\"chevronRight\" :size=\"12\" :class=\"{ 'cv__fold--open': !folded.has(i) }\" />\n </button>\n <span v-else class=\"cv__fold cv__fold--empty\" />\n <span class=\"cv__ln\">{{ i + 1 }}</span>\n </span>\n <code class=\"cv__code hljs\" v-html=\"lineHtml[i]\" />\n <button\n v-if=\"folded.has(i)\"\n type=\"button\"\n class=\"cv__ellipsis\"\n :title=\"`展开折叠的 ${(regions.get(i) ?? i) - i} 行`\"\n @click=\"toggle(i)\"\n >\n ⋯\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.cv {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Floating tools bar — top-right, above the code (highlight-language picker + format toggle). */\n.cv__tools {\n position: absolute;\n top: 8px;\n right: 14px;\n z-index: 3;\n display: flex;\n align-items: flex-start;\n gap: 6px;\n}\n\n/* Shared small pill used by both floating tools. */\n.cv__toolbtn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n max-width: 190px;\n padding: 3px 10px;\n font-family: inherit;\n font-size: 11.5px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n cursor: pointer;\n box-shadow: var(--shadow);\n transition:\n background 0.15s,\n color 0.15s,\n border-color 0.15s;\n}\n.cv__toolbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.cv__toolbtn span {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cv__toolbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.cv__toolbtn--on:hover {\n color: #fff;\n}\n\n/* Format toggle specifics (disabled + error states). */\n.cv__fmt:disabled {\n cursor: default;\n opacity: 0.7;\n}\n.cv__fmt--err {\n color: var(--danger);\n border-color: var(--danger);\n}\n\n/* Highlight-language picker dropdown (anchored under its pill, right-aligned). */\n.cv__lang {\n position: relative;\n display: inline-flex;\n}\n.cv__langpop {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 4;\n width: 224px;\n padding: 7px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 10px;\n box-shadow: var(--shadow);\n}\n.cv__langsearch {\n width: 100%;\n box-sizing: border-box;\n padding: 6px 9px;\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n outline: none;\n}\n.cv__langsearch:focus {\n border-color: var(--accent);\n}\n.cv__langlist {\n max-height: 288px;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: 6px;\n}\n.cv__langitem {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 6px 8px;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--text);\n font-family: inherit;\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n transition:\n background 0.12s,\n color 0.12s;\n}\n.cv__langitem:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.cv__langitem--on {\n color: var(--accent);\n}\n.cv__langitem-label {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.cv__langitem-check {\n flex-shrink: 0;\n color: var(--accent);\n}\n.cv__langempty {\n margin: 0;\n padding: 10px 8px;\n font-size: 12px;\n color: var(--faint);\n text-align: center;\n}\n\n.cv__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n\n/* Foldable per-line view -------------------------------------------------------------------------- */\n.cv__rows {\n min-width: max-content;\n padding: 14px 0;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n}\n.cv__row {\n display: flex;\n align-items: flex-start;\n}\n/* Gutter (fold chevron + line number) sticks to the left so it stays put while long lines scroll. */\n.cv__gut {\n position: sticky;\n left: 0;\n z-index: 1;\n flex-shrink: 0;\n display: flex;\n align-items: flex-start;\n background: var(--code-bg);\n user-select: none;\n}\n.cv__fold {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 16px;\n height: var(--fv-code-lh, 21px);\n padding: 0;\n border: none;\n background: transparent;\n color: var(--faint);\n cursor: pointer;\n}\n.cv__fold--empty {\n cursor: default;\n}\n.cv__fold :deep(svg) {\n transition: transform 0.12s;\n}\n.cv__fold--open {\n transform: rotate(90deg);\n}\n.cv__fold:hover :deep(svg) {\n color: var(--accent);\n}\n.cv__ln {\n flex-shrink: 0;\n width: 48px;\n padding: 0 12px 0 4px;\n text-align: right;\n color: var(--faint);\n}\n.cv__code {\n flex: 1 1 auto;\n padding: 0 18px 0 14px;\n white-space: pre;\n color: var(--text);\n background: transparent;\n border: none;\n font-family: inherit;\n tab-size: 2;\n}\n.cv__ellipsis {\n align-self: center;\n margin: 0 0 0 2px;\n padding: 0 7px;\n font-family: inherit;\n line-height: 1.3;\n color: var(--muted);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 5px;\n cursor: pointer;\n}\n.cv__ellipsis:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Huge-file blob fallback (mirrors the panel's original source layout) ----------------------------- */\n.cv__scroll--blob {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.cv__codewrap {\n display: flex;\n align-items: stretch;\n min-height: 100%;\n padding: 14px 0;\n}\n.cv__gutter {\n flex-shrink: 0;\n margin: 0;\n padding: 0 12px 0 16px;\n border-right: 1px solid var(--code-border);\n text-align: right;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n color: var(--faint);\n user-select: none;\n white-space: pre;\n}\n.cv__codecol {\n flex: 1 1 auto;\n min-width: 0;\n overflow-x: auto;\n}\n.cv__pre {\n margin: 0;\n padding: 0 18px 0 14px;\n font-family: var(--font-mono);\n font-size: var(--fv-code-fs, 12.5px);\n line-height: var(--fv-code-lh, 21px);\n color: var(--text);\n white-space: pre;\n tab-size: 2;\n}\n.cv__pre code {\n font-family: inherit;\n background: transparent;\n border: none;\n padding: 0;\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./CsvViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-19d40e74"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=CsvViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CsvViewer.js","names":[],"sources":["../../src/components/CsvViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { fileExt } from '@/lib/fileType'\nimport { parseCsv } from '@/lib/csv'\n\n/**\n * Side-viewer CSV / TSV table. Consumes the text the panel already fetched (CSV is plain text — no second\n * download, same as the subtitle / geo viewers) and parses it with the hand-rolled RFC 4180 parser\n * (see lib/csv.ts: quoted fields, doubled-quote escaping, embedded delimiters/newlines, delimiter sniffing).\n *\n * The first row can be treated as a header (toggle). Cell text is rendered via mustache, so Vue escapes it —\n * a cell containing `<script>` or other markup is shown literally, never executed. Only small CSVs reach here\n * (larger ones stream through the log viewer, see isCsvFile / isLogFile); even so the body is capped at the\n * first {@link RENDER_CAP} rows until you ask for all, so a dense file doesn't paint tens of thousands of\n * cells up front.\n *\n * Three affordances on top of the plain table:\n * - hover a cell (delegated `mouseover` on the scroll box) and, after a short intent delay, a floating popover\n * shows the cell's full text — the table cells clip with ellipsis, the popover doesn't;\n * - the popover is reachable: it overlaps the cell's bottom edge and cancels its own hide timer on enter, so you\n * can move the mouse in to select text or hit 「复制」 (writes the exact cell string to the clipboard);\n * - drag the thin handle on a column header's right edge to resize that column. Widths default to a cheap\n * content-based estimate ({@link autoWidths}); a fixed table layout makes the drag pixel-exact.\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst parsed = computed(() => parseCsv(props.text, fileExt(props.name)))\nconst hasHeader = ref(true)\nconst showAll = ref(false)\nconst RENDER_CAP = 1000\n\nconst delimLabel = computed(() => {\n switch (parsed.value.delimiter) {\n case '\\t':\n return 'Tab'\n case ';':\n return '分号'\n case '|':\n return '竖线'\n default:\n return '逗号'\n }\n})\n\nconst colRange = computed(() => Array.from({ length: parsed.value.cols }, (_, i) => i))\n// Header cells: the first data row when the toggle is on, else spreadsheet-style letters (A, B, …) so the\n// resize handles always have a home even when the file has no header row.\nconst headerCells = computed(() =>\n colRange.value.map((c) => (hasHeader.value ? (parsed.value.rows[0]?.[c] ?? '') : colName(c))),\n)\nconst bodyRows = computed(() => (hasHeader.value ? parsed.value.rows.slice(1) : parsed.value.rows))\nconst visibleBody = computed(() =>\n showAll.value ? bodyRows.value : bodyRows.value.slice(0, RENDER_CAP),\n)\nconst capped = computed(() => !showAll.value && bodyRows.value.length > RENDER_CAP)\n\nfunction colName(i: number): string {\n let s = ''\n let n = i\n do {\n s = String.fromCharCode(65 + (n % 26)) + s\n n = Math.floor(n / 26) - 1\n } while (n >= 0)\n return s\n}\n\n// Original 1-based line number for a body row (account for the header row when present).\nfunction lineNo(bodyIndex: number): number {\n return hasHeader.value ? bodyIndex + 2 : bodyIndex + 1\n}\n\n/* ------------------------------------------------------------------ column widths / resize ---------- */\n\nconst MIN_COL_W = 48\nconst AUTO_MIN = 56\nconst AUTO_MAX = 360\nconst AUTO_SAMPLE = 120\nconst CHAR_PX = 7\n\n// East-Asian & full-width code points render ~2× as wide as Latin — count them double so CJK columns get a\n// sensible default instead of being under-sized and clipped from the start.\nfunction displayLen(s: string): number {\n let n = 0\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i)\n const wide =\n (code >= 0x1100 && code <= 0x115f) ||\n (code >= 0x2e80 && code <= 0xa4cf) ||\n (code >= 0xac00 && code <= 0xd7a3) ||\n (code >= 0xf900 && code <= 0xfaff) ||\n (code >= 0xff00 && code <= 0xff60) ||\n (code >= 0xffe0 && code <= 0xffe6)\n n += wide ? 2 : 1\n }\n return n\n}\n\n// Content-based default width per column, sampled over the first rows so a huge file stays cheap. User drags\n// override this (see colWidths); the estimate only decides the initial layout.\nconst autoWidths = computed(() => {\n const cols = parsed.value.cols\n const rows = parsed.value.rows\n const widths = Array.from<number>({ length: cols }).fill(AUTO_MIN)\n const sample = Math.min(rows.length, AUTO_SAMPLE)\n for (let r = 0; r < sample; r++) {\n const row = rows[r]\n if (!row) continue\n for (let c = 0; c < cols; c++) {\n const v = row[c]\n if (!v) continue\n const w = Math.min(AUTO_MAX, Math.max(AUTO_MIN, displayLen(v) * CHAR_PX + 22))\n if (w > widths[c]) widths[c] = w\n }\n }\n return widths\n})\n\n// Explicit per-column overrides from resize drags; null = fall back to the auto estimate. Reset when the file\n// (props.text) changes so a new file doesn't inherit the previous one's drags.\nconst colWidths = ref<(number | null)[]>([])\nwatch(\n () => props.text,\n () => {\n colWidths.value = Array.from<number | null>({ length: parsed.value.cols }).fill(null)\n },\n { immediate: true },\n)\n\nconst effWidths = computed(() =>\n colRange.value.map((c) => colWidths.value[c] ?? autoWidths.value[c] ?? AUTO_MIN),\n)\n// Row-number gutter widens with the largest line number so 5–6 digit numbers don't clip.\nconst gutterW = computed(() => {\n const digits = String(Math.max(1, parsed.value.rows.length)).length\n return Math.max(46, 20 + digits * 8)\n})\nconst tableWidth = computed(() => gutterW.value + effWidths.value.reduce((a, w) => a + w, 0))\n\nconst resizing = ref(false)\nlet resizeCol = -1\nlet resizeStartX = 0\nlet resizeStartW = 0\n\nfunction startResize(c: number, e: MouseEvent): void {\n resizeCol = c\n resizeStartX = e.clientX\n resizeStartW = effWidths.value[c]\n resizing.value = true\n closePop()\n window.addEventListener('mousemove', onResizeMove)\n window.addEventListener('mouseup', endResize)\n document.body.style.userSelect = 'none'\n document.body.style.cursor = 'col-resize'\n}\nfunction onResizeMove(e: MouseEvent): void {\n if (resizeCol < 0) return\n colWidths.value[resizeCol] = Math.max(\n MIN_COL_W,\n Math.round(resizeStartW + (e.clientX - resizeStartX)),\n )\n}\nfunction endResize(): void {\n resizeCol = -1\n resizing.value = false\n window.removeEventListener('mousemove', onResizeMove)\n window.removeEventListener('mouseup', endResize)\n document.body.style.userSelect = ''\n document.body.style.cursor = ''\n}\n\n/* ------------------------------------------------------------------ hover popover + copy ------------- */\n\nconst HOVER_SHOW_MS = 130\nconst HOVER_HIDE_MS = 160\n\ninterface PopState {\n text: string\n left: number\n top: number\n}\nconst pop = ref<PopState | null>(null)\nconst copied = ref(false)\nlet showTimer: ReturnType<typeof setTimeout> | undefined\nlet hideTimer: ReturnType<typeof setTimeout> | undefined\nlet hoveredCell: HTMLElement | null = null\n\nfunction placePop(cell: HTMLElement, text: string): void {\n const rect = cell.getBoundingClientRect()\n const maxW = 440\n const margin = 8\n let left = rect.left\n if (left + maxW > window.innerWidth - margin) {\n left = Math.max(margin, window.innerWidth - margin - maxW)\n }\n // top overlaps the cell's bottom edge by 1px so the pointer can travel into the popover without a gap.\n pop.value = { text, left, top: rect.bottom - 1 }\n copied.value = false\n}\n\n// Whether a cell's text is actually truncated (clipped by ellipsis / overflow). The clipping element is the\n// header cell's inner text span, or the body cell itself. +1 tolerance guards against sub-pixel rounding.\nfunction isClipped(cell: HTMLElement): boolean {\n const clip = cell.querySelector<HTMLElement>('.csv__th-text') ?? cell\n return clip.scrollWidth - clip.clientWidth > 1 || clip.scrollHeight - clip.clientHeight > 1\n}\n\nfunction onCellOver(e: MouseEvent): void {\n if (resizing.value) return\n const cell = (e.target as HTMLElement).closest<HTMLElement>('.csv__cell')\n if (!cell || cell === hoveredCell) return\n hoveredCell = cell\n if (showTimer) {\n clearTimeout(showTimer)\n showTimer = undefined\n }\n const text = cell.textContent ?? ''\n // Only worth a popover when the text is actually cut off — a fully-visible cell needs no overlay. Let any\n // currently-open popover fade out as we move onto such a cell.\n if (!text || !isClipped(cell)) {\n scheduleHide()\n return\n }\n if (hideTimer) {\n clearTimeout(hideTimer)\n hideTimer = undefined\n }\n // Already open → swap to the new cell instantly; first hover waits out the intent delay.\n if (pop.value) placePop(cell, text)\n else showTimer = setTimeout(() => placePop(cell, text), HOVER_SHOW_MS)\n}\n\nfunction scheduleHide(): void {\n if (showTimer) {\n clearTimeout(showTimer)\n showTimer = undefined\n }\n if (hideTimer) clearTimeout(hideTimer)\n hideTimer = setTimeout(() => {\n pop.value = null\n hoveredCell = null\n hideTimer = undefined\n }, HOVER_HIDE_MS)\n}\n\nfunction keepPop(): void {\n if (hideTimer) {\n clearTimeout(hideTimer)\n hideTimer = undefined\n }\n}\n\nfunction closePop(): void {\n if (showTimer) {\n clearTimeout(showTimer)\n showTimer = undefined\n }\n if (hideTimer) {\n clearTimeout(hideTimer)\n hideTimer = undefined\n }\n pop.value = null\n hoveredCell = null\n}\n\nasync function copyPop(): Promise<void> {\n if (!pop.value) return\n try {\n await navigator.clipboard.writeText(pop.value.text)\n copied.value = true\n } catch {\n copied.value = false\n }\n}\n\nonBeforeUnmount(() => {\n closePop()\n endResize()\n})\n</script>\n\n<template>\n <div class=\"csv\">\n <div class=\"csv__bar\">\n <span class=\"csv__count\">{{ parsed.rows.length }} 行 · {{ parsed.cols }} 列</span>\n <span class=\"csv__delim\">分隔符:{{ delimLabel }}</span>\n <label class=\"csv__toggle\">\n <input v-model=\"hasHeader\" type=\"checkbox\" />\n 首行为表头\n </label>\n <span class=\"csv__spacer\" />\n <button v-if=\"capped\" type=\"button\" class=\"csv__more\" @click=\"showAll = true\">\n 显示全部 {{ bodyRows.length }} 行\n </button>\n </div>\n\n <div class=\"csv__scroll\" @mouseover=\"onCellOver\" @mouseleave=\"scheduleHide\">\n <table v-if=\"parsed.rows.length\" class=\"csv__table\" :style=\"{ width: tableWidth + 'px' }\">\n <colgroup>\n <col :style=\"{ width: gutterW + 'px' }\" />\n <col v-for=\"c in colRange\" :key=\"c\" :style=\"{ width: effWidths[c] + 'px' }\" />\n </colgroup>\n <thead>\n <tr>\n <th class=\"csv__rownum csv__corner\" aria-hidden=\"true\" />\n <th\n v-for=\"c in colRange\"\n :key=\"c\"\n class=\"csv__th\"\n :class=\"{ csv__cell: hasHeader, 'csv__th--auto': !hasHeader }\"\n >\n <span class=\"csv__th-text\">{{ headerCells[c] }}</span>\n <span\n class=\"csv__resize\"\n title=\"拖动调整列宽\"\n @mousedown.prevent.stop=\"startResize(c, $event)\"\n />\n </th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(row, r) in visibleBody\" :key=\"r\" class=\"csv__tr\">\n <td class=\"csv__rownum\">{{ lineNo(r) }}</td>\n <td v-for=\"c in colRange\" :key=\"c\" class=\"csv__td csv__cell\">{{ row[c] ?? '' }}</td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"csv__empty\">空文件</p>\n\n <p v-if=\"capped\" class=\"csv__capnote\">\n 已显示前 {{ RENDER_CAP }} 行,共 {{ bodyRows.length }} 行 —— 点上方「显示全部」加载其余。\n </p>\n </div>\n\n <Teleport to=\"body\">\n <div\n v-if=\"pop\"\n class=\"csv-pop\"\n :style=\"{ left: pop.left + 'px', top: pop.top + 'px' }\"\n @mouseenter=\"keepPop\"\n @mouseleave=\"scheduleHide\"\n >\n <div class=\"csv-pop__text\">{{ pop.text }}</div>\n <div class=\"csv-pop__foot\">\n <button type=\"button\" class=\"csv-pop__copy\" @click=\"copyPop\">\n {{ copied ? '已复制' : '复制' }}\n </button>\n </div>\n </div>\n </Teleport>\n </div>\n</template>\n\n<style scoped>\n.csv {\n flex: 1 1 auto;\n /* Row flex item inside .fvb__fill — needs min-width:0 (not just min-height) so the wide fixed-layout table\n lets .csv__scroll clip-and-scroll instead of stretching .csv past the panel (which hid both scrollbars). */\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.csv__bar {\n display: flex;\n align-items: center;\n gap: 14px;\n flex-shrink: 0;\n padding: 8px 14px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n font-size: 12px;\n color: var(--faint);\n}\n.csv__count {\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n}\n.csv__toggle {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n cursor: pointer;\n user-select: none;\n}\n.csv__toggle input {\n cursor: pointer;\n}\n.csv__spacer {\n flex: 1 1 auto;\n}\n.csv__more {\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.csv__more:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.csv__scroll {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n\n.csv__table {\n border-collapse: separate;\n border-spacing: 0;\n table-layout: fixed;\n font-size: 12.5px;\n color: var(--text);\n}\n.csv__th,\n.csv__td {\n padding: 5px 10px;\n border-right: 1px solid var(--code-border);\n border-bottom: 1px solid var(--code-border);\n text-align: left;\n white-space: pre;\n overflow: hidden;\n text-overflow: ellipsis;\n vertical-align: top;\n}\n.csv__th {\n position: sticky;\n top: 0;\n z-index: 2;\n padding-right: 12px;\n font-weight: 600;\n color: var(--text-strong);\n background: var(--surface-2);\n border-bottom: 1px solid var(--border-2);\n}\n/* Synthetic A/B/C labels shown when the first row is treated as data, not a header. */\n.csv__th--auto {\n font-weight: 500;\n color: var(--faint);\n}\n.csv__th-text {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: pre;\n}\n.csv__tr:nth-child(even) .csv__td {\n background: color-mix(in srgb, var(--surface-2) 45%, transparent);\n}\n/* Column-resize handle: sits over the header cell's right edge; the grip line fades in on hover. */\n.csv__resize {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 9px;\n cursor: col-resize;\n user-select: none;\n touch-action: none;\n}\n.csv__resize::after {\n content: '';\n position: absolute;\n top: 22%;\n bottom: 22%;\n right: 3px;\n width: 2px;\n border-radius: 1px;\n background: var(--border-2);\n opacity: 0;\n transition: opacity 0.12s ease;\n}\n.csv__th:hover .csv__resize::after {\n opacity: 1;\n}\n.csv__resize:hover::after {\n opacity: 1;\n background: var(--accent);\n}\n/* Row-number gutter: sticky on the left so it stays visible while scrolling wide tables. */\n.csv__rownum {\n position: sticky;\n left: 0;\n z-index: 1;\n padding: 5px 10px;\n border-right: 1px solid var(--code-border);\n border-bottom: 1px solid var(--code-border);\n text-align: right;\n color: var(--faint);\n background: var(--code-bg);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n.csv__tr:nth-child(even) .csv__rownum {\n background: color-mix(in srgb, var(--surface-2) 45%, var(--code-bg));\n}\n/* Top-left corner cell sits at both sticky axes, so it needs to win the z stack. */\n.csv__corner {\n top: 0;\n z-index: 3;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border-2);\n}\n\n.csv__empty {\n padding: 20px 16px;\n color: var(--faint);\n font-size: 13px;\n}\n.csv__capnote {\n padding: 12px 16px 20px;\n color: var(--faint);\n font-size: 12px;\n}\n\n/* Hover popover — teleported to <body>, so it escapes the scroll box's clipping and stacking. Scoped styles\n still apply (Vue stamps the data-v attribute on teleported nodes). */\n.csv-pop {\n position: fixed;\n z-index: 3000;\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 120px;\n max-width: 440px;\n padding: 8px 10px;\n border: 1px solid var(--border-2);\n border-radius: 8px;\n background: var(--surface);\n box-shadow: 0 8px 26px rgba(0, 0, 0, 0.2);\n font-size: 12.5px;\n color: var(--text);\n}\n.csv-pop__text {\n max-height: 320px;\n overflow: auto;\n overscroll-behavior: contain;\n white-space: pre-wrap;\n word-break: break-word;\n line-height: 1.5;\n user-select: text;\n}\n.csv-pop__foot {\n display: flex;\n justify-content: flex-end;\n}\n.csv-pop__copy {\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n padding: 3px 12px;\n cursor: pointer;\n}\n.csv-pop__copy:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":""}
@@ -185,3 +185,5 @@ var b = { class: "csv" }, x = { class: "csv__bar" }, ne = { class: "csv__count"
185
185
  });
186
186
  //#endregion
187
187
  export { w as default };
188
+
189
+ //# sourceMappingURL=CsvViewer.vue_vue_type_script_setup_true_lang.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CsvViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/CsvViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { fileExt } from '@/lib/fileType'\nimport { parseCsv } from '@/lib/csv'\n\n/**\n * Side-viewer CSV / TSV table. Consumes the text the panel already fetched (CSV is plain text — no second\n * download, same as the subtitle / geo viewers) and parses it with the hand-rolled RFC 4180 parser\n * (see lib/csv.ts: quoted fields, doubled-quote escaping, embedded delimiters/newlines, delimiter sniffing).\n *\n * The first row can be treated as a header (toggle). Cell text is rendered via mustache, so Vue escapes it —\n * a cell containing `<script>` or other markup is shown literally, never executed. Only small CSVs reach here\n * (larger ones stream through the log viewer, see isCsvFile / isLogFile); even so the body is capped at the\n * first {@link RENDER_CAP} rows until you ask for all, so a dense file doesn't paint tens of thousands of\n * cells up front.\n *\n * Three affordances on top of the plain table:\n * - hover a cell (delegated `mouseover` on the scroll box) and, after a short intent delay, a floating popover\n * shows the cell's full text — the table cells clip with ellipsis, the popover doesn't;\n * - the popover is reachable: it overlaps the cell's bottom edge and cancels its own hide timer on enter, so you\n * can move the mouse in to select text or hit 「复制」 (writes the exact cell string to the clipboard);\n * - drag the thin handle on a column header's right edge to resize that column. Widths default to a cheap\n * content-based estimate ({@link autoWidths}); a fixed table layout makes the drag pixel-exact.\n */\nconst props = defineProps<{ text: string; name: string }>()\n\nconst parsed = computed(() => parseCsv(props.text, fileExt(props.name)))\nconst hasHeader = ref(true)\nconst showAll = ref(false)\nconst RENDER_CAP = 1000\n\nconst delimLabel = computed(() => {\n switch (parsed.value.delimiter) {\n case '\\t':\n return 'Tab'\n case ';':\n return '分号'\n case '|':\n return '竖线'\n default:\n return '逗号'\n }\n})\n\nconst colRange = computed(() => Array.from({ length: parsed.value.cols }, (_, i) => i))\n// Header cells: the first data row when the toggle is on, else spreadsheet-style letters (A, B, …) so the\n// resize handles always have a home even when the file has no header row.\nconst headerCells = computed(() =>\n colRange.value.map((c) => (hasHeader.value ? (parsed.value.rows[0]?.[c] ?? '') : colName(c))),\n)\nconst bodyRows = computed(() => (hasHeader.value ? parsed.value.rows.slice(1) : parsed.value.rows))\nconst visibleBody = computed(() =>\n showAll.value ? bodyRows.value : bodyRows.value.slice(0, RENDER_CAP),\n)\nconst capped = computed(() => !showAll.value && bodyRows.value.length > RENDER_CAP)\n\nfunction colName(i: number): string {\n let s = ''\n let n = i\n do {\n s = String.fromCharCode(65 + (n % 26)) + s\n n = Math.floor(n / 26) - 1\n } while (n >= 0)\n return s\n}\n\n// Original 1-based line number for a body row (account for the header row when present).\nfunction lineNo(bodyIndex: number): number {\n return hasHeader.value ? bodyIndex + 2 : bodyIndex + 1\n}\n\n/* ------------------------------------------------------------------ column widths / resize ---------- */\n\nconst MIN_COL_W = 48\nconst AUTO_MIN = 56\nconst AUTO_MAX = 360\nconst AUTO_SAMPLE = 120\nconst CHAR_PX = 7\n\n// East-Asian & full-width code points render ~2× as wide as Latin — count them double so CJK columns get a\n// sensible default instead of being under-sized and clipped from the start.\nfunction displayLen(s: string): number {\n let n = 0\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i)\n const wide =\n (code >= 0x1100 && code <= 0x115f) ||\n (code >= 0x2e80 && code <= 0xa4cf) ||\n (code >= 0xac00 && code <= 0xd7a3) ||\n (code >= 0xf900 && code <= 0xfaff) ||\n (code >= 0xff00 && code <= 0xff60) ||\n (code >= 0xffe0 && code <= 0xffe6)\n n += wide ? 2 : 1\n }\n return n\n}\n\n// Content-based default width per column, sampled over the first rows so a huge file stays cheap. User drags\n// override this (see colWidths); the estimate only decides the initial layout.\nconst autoWidths = computed(() => {\n const cols = parsed.value.cols\n const rows = parsed.value.rows\n const widths = Array.from<number>({ length: cols }).fill(AUTO_MIN)\n const sample = Math.min(rows.length, AUTO_SAMPLE)\n for (let r = 0; r < sample; r++) {\n const row = rows[r]\n if (!row) continue\n for (let c = 0; c < cols; c++) {\n const v = row[c]\n if (!v) continue\n const w = Math.min(AUTO_MAX, Math.max(AUTO_MIN, displayLen(v) * CHAR_PX + 22))\n if (w > widths[c]) widths[c] = w\n }\n }\n return widths\n})\n\n// Explicit per-column overrides from resize drags; null = fall back to the auto estimate. Reset when the file\n// (props.text) changes so a new file doesn't inherit the previous one's drags.\nconst colWidths = ref<(number | null)[]>([])\nwatch(\n () => props.text,\n () => {\n colWidths.value = Array.from<number | null>({ length: parsed.value.cols }).fill(null)\n },\n { immediate: true },\n)\n\nconst effWidths = computed(() =>\n colRange.value.map((c) => colWidths.value[c] ?? autoWidths.value[c] ?? AUTO_MIN),\n)\n// Row-number gutter widens with the largest line number so 5–6 digit numbers don't clip.\nconst gutterW = computed(() => {\n const digits = String(Math.max(1, parsed.value.rows.length)).length\n return Math.max(46, 20 + digits * 8)\n})\nconst tableWidth = computed(() => gutterW.value + effWidths.value.reduce((a, w) => a + w, 0))\n\nconst resizing = ref(false)\nlet resizeCol = -1\nlet resizeStartX = 0\nlet resizeStartW = 0\n\nfunction startResize(c: number, e: MouseEvent): void {\n resizeCol = c\n resizeStartX = e.clientX\n resizeStartW = effWidths.value[c]\n resizing.value = true\n closePop()\n window.addEventListener('mousemove', onResizeMove)\n window.addEventListener('mouseup', endResize)\n document.body.style.userSelect = 'none'\n document.body.style.cursor = 'col-resize'\n}\nfunction onResizeMove(e: MouseEvent): void {\n if (resizeCol < 0) return\n colWidths.value[resizeCol] = Math.max(\n MIN_COL_W,\n Math.round(resizeStartW + (e.clientX - resizeStartX)),\n )\n}\nfunction endResize(): void {\n resizeCol = -1\n resizing.value = false\n window.removeEventListener('mousemove', onResizeMove)\n window.removeEventListener('mouseup', endResize)\n document.body.style.userSelect = ''\n document.body.style.cursor = ''\n}\n\n/* ------------------------------------------------------------------ hover popover + copy ------------- */\n\nconst HOVER_SHOW_MS = 130\nconst HOVER_HIDE_MS = 160\n\ninterface PopState {\n text: string\n left: number\n top: number\n}\nconst pop = ref<PopState | null>(null)\nconst copied = ref(false)\nlet showTimer: ReturnType<typeof setTimeout> | undefined\nlet hideTimer: ReturnType<typeof setTimeout> | undefined\nlet hoveredCell: HTMLElement | null = null\n\nfunction placePop(cell: HTMLElement, text: string): void {\n const rect = cell.getBoundingClientRect()\n const maxW = 440\n const margin = 8\n let left = rect.left\n if (left + maxW > window.innerWidth - margin) {\n left = Math.max(margin, window.innerWidth - margin - maxW)\n }\n // top overlaps the cell's bottom edge by 1px so the pointer can travel into the popover without a gap.\n pop.value = { text, left, top: rect.bottom - 1 }\n copied.value = false\n}\n\n// Whether a cell's text is actually truncated (clipped by ellipsis / overflow). The clipping element is the\n// header cell's inner text span, or the body cell itself. +1 tolerance guards against sub-pixel rounding.\nfunction isClipped(cell: HTMLElement): boolean {\n const clip = cell.querySelector<HTMLElement>('.csv__th-text') ?? cell\n return clip.scrollWidth - clip.clientWidth > 1 || clip.scrollHeight - clip.clientHeight > 1\n}\n\nfunction onCellOver(e: MouseEvent): void {\n if (resizing.value) return\n const cell = (e.target as HTMLElement).closest<HTMLElement>('.csv__cell')\n if (!cell || cell === hoveredCell) return\n hoveredCell = cell\n if (showTimer) {\n clearTimeout(showTimer)\n showTimer = undefined\n }\n const text = cell.textContent ?? ''\n // Only worth a popover when the text is actually cut off — a fully-visible cell needs no overlay. Let any\n // currently-open popover fade out as we move onto such a cell.\n if (!text || !isClipped(cell)) {\n scheduleHide()\n return\n }\n if (hideTimer) {\n clearTimeout(hideTimer)\n hideTimer = undefined\n }\n // Already open → swap to the new cell instantly; first hover waits out the intent delay.\n if (pop.value) placePop(cell, text)\n else showTimer = setTimeout(() => placePop(cell, text), HOVER_SHOW_MS)\n}\n\nfunction scheduleHide(): void {\n if (showTimer) {\n clearTimeout(showTimer)\n showTimer = undefined\n }\n if (hideTimer) clearTimeout(hideTimer)\n hideTimer = setTimeout(() => {\n pop.value = null\n hoveredCell = null\n hideTimer = undefined\n }, HOVER_HIDE_MS)\n}\n\nfunction keepPop(): void {\n if (hideTimer) {\n clearTimeout(hideTimer)\n hideTimer = undefined\n }\n}\n\nfunction closePop(): void {\n if (showTimer) {\n clearTimeout(showTimer)\n showTimer = undefined\n }\n if (hideTimer) {\n clearTimeout(hideTimer)\n hideTimer = undefined\n }\n pop.value = null\n hoveredCell = null\n}\n\nasync function copyPop(): Promise<void> {\n if (!pop.value) return\n try {\n await navigator.clipboard.writeText(pop.value.text)\n copied.value = true\n } catch {\n copied.value = false\n }\n}\n\nonBeforeUnmount(() => {\n closePop()\n endResize()\n})\n</script>\n\n<template>\n <div class=\"csv\">\n <div class=\"csv__bar\">\n <span class=\"csv__count\">{{ parsed.rows.length }} 行 · {{ parsed.cols }} 列</span>\n <span class=\"csv__delim\">分隔符:{{ delimLabel }}</span>\n <label class=\"csv__toggle\">\n <input v-model=\"hasHeader\" type=\"checkbox\" />\n 首行为表头\n </label>\n <span class=\"csv__spacer\" />\n <button v-if=\"capped\" type=\"button\" class=\"csv__more\" @click=\"showAll = true\">\n 显示全部 {{ bodyRows.length }} 行\n </button>\n </div>\n\n <div class=\"csv__scroll\" @mouseover=\"onCellOver\" @mouseleave=\"scheduleHide\">\n <table v-if=\"parsed.rows.length\" class=\"csv__table\" :style=\"{ width: tableWidth + 'px' }\">\n <colgroup>\n <col :style=\"{ width: gutterW + 'px' }\" />\n <col v-for=\"c in colRange\" :key=\"c\" :style=\"{ width: effWidths[c] + 'px' }\" />\n </colgroup>\n <thead>\n <tr>\n <th class=\"csv__rownum csv__corner\" aria-hidden=\"true\" />\n <th\n v-for=\"c in colRange\"\n :key=\"c\"\n class=\"csv__th\"\n :class=\"{ csv__cell: hasHeader, 'csv__th--auto': !hasHeader }\"\n >\n <span class=\"csv__th-text\">{{ headerCells[c] }}</span>\n <span\n class=\"csv__resize\"\n title=\"拖动调整列宽\"\n @mousedown.prevent.stop=\"startResize(c, $event)\"\n />\n </th>\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"(row, r) in visibleBody\" :key=\"r\" class=\"csv__tr\">\n <td class=\"csv__rownum\">{{ lineNo(r) }}</td>\n <td v-for=\"c in colRange\" :key=\"c\" class=\"csv__td csv__cell\">{{ row[c] ?? '' }}</td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"csv__empty\">空文件</p>\n\n <p v-if=\"capped\" class=\"csv__capnote\">\n 已显示前 {{ RENDER_CAP }} 行,共 {{ bodyRows.length }} 行 —— 点上方「显示全部」加载其余。\n </p>\n </div>\n\n <Teleport to=\"body\">\n <div\n v-if=\"pop\"\n class=\"csv-pop\"\n :style=\"{ left: pop.left + 'px', top: pop.top + 'px' }\"\n @mouseenter=\"keepPop\"\n @mouseleave=\"scheduleHide\"\n >\n <div class=\"csv-pop__text\">{{ pop.text }}</div>\n <div class=\"csv-pop__foot\">\n <button type=\"button\" class=\"csv-pop__copy\" @click=\"copyPop\">\n {{ copied ? '已复制' : '复制' }}\n </button>\n </div>\n </div>\n </Teleport>\n </div>\n</template>\n\n<style scoped>\n.csv {\n flex: 1 1 auto;\n /* Row flex item inside .fvb__fill — needs min-width:0 (not just min-height) so the wide fixed-layout table\n lets .csv__scroll clip-and-scroll instead of stretching .csv past the panel (which hid both scrollbars). */\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.csv__bar {\n display: flex;\n align-items: center;\n gap: 14px;\n flex-shrink: 0;\n padding: 8px 14px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n font-size: 12px;\n color: var(--faint);\n}\n.csv__count {\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n}\n.csv__toggle {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n cursor: pointer;\n user-select: none;\n}\n.csv__toggle input {\n cursor: pointer;\n}\n.csv__spacer {\n flex: 1 1 auto;\n}\n.csv__more {\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n}\n.csv__more:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.csv__scroll {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n\n.csv__table {\n border-collapse: separate;\n border-spacing: 0;\n table-layout: fixed;\n font-size: 12.5px;\n color: var(--text);\n}\n.csv__th,\n.csv__td {\n padding: 5px 10px;\n border-right: 1px solid var(--code-border);\n border-bottom: 1px solid var(--code-border);\n text-align: left;\n white-space: pre;\n overflow: hidden;\n text-overflow: ellipsis;\n vertical-align: top;\n}\n.csv__th {\n position: sticky;\n top: 0;\n z-index: 2;\n padding-right: 12px;\n font-weight: 600;\n color: var(--text-strong);\n background: var(--surface-2);\n border-bottom: 1px solid var(--border-2);\n}\n/* Synthetic A/B/C labels shown when the first row is treated as data, not a header. */\n.csv__th--auto {\n font-weight: 500;\n color: var(--faint);\n}\n.csv__th-text {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: pre;\n}\n.csv__tr:nth-child(even) .csv__td {\n background: color-mix(in srgb, var(--surface-2) 45%, transparent);\n}\n/* Column-resize handle: sits over the header cell's right edge; the grip line fades in on hover. */\n.csv__resize {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 9px;\n cursor: col-resize;\n user-select: none;\n touch-action: none;\n}\n.csv__resize::after {\n content: '';\n position: absolute;\n top: 22%;\n bottom: 22%;\n right: 3px;\n width: 2px;\n border-radius: 1px;\n background: var(--border-2);\n opacity: 0;\n transition: opacity 0.12s ease;\n}\n.csv__th:hover .csv__resize::after {\n opacity: 1;\n}\n.csv__resize:hover::after {\n opacity: 1;\n background: var(--accent);\n}\n/* Row-number gutter: sticky on the left so it stays visible while scrolling wide tables. */\n.csv__rownum {\n position: sticky;\n left: 0;\n z-index: 1;\n padding: 5px 10px;\n border-right: 1px solid var(--code-border);\n border-bottom: 1px solid var(--code-border);\n text-align: right;\n color: var(--faint);\n background: var(--code-bg);\n font-variant-numeric: tabular-nums;\n user-select: none;\n}\n.csv__tr:nth-child(even) .csv__rownum {\n background: color-mix(in srgb, var(--surface-2) 45%, var(--code-bg));\n}\n/* Top-left corner cell sits at both sticky axes, so it needs to win the z stack. */\n.csv__corner {\n top: 0;\n z-index: 3;\n background: var(--surface-2);\n border-bottom: 1px solid var(--border-2);\n}\n\n.csv__empty {\n padding: 20px 16px;\n color: var(--faint);\n font-size: 13px;\n}\n.csv__capnote {\n padding: 12px 16px 20px;\n color: var(--faint);\n font-size: 12px;\n}\n\n/* Hover popover — teleported to <body>, so it escapes the scroll box's clipping and stacking. Scoped styles\n still apply (Vue stamps the data-v attribute on teleported nodes). */\n.csv-pop {\n position: fixed;\n z-index: 3000;\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 120px;\n max-width: 440px;\n padding: 8px 10px;\n border: 1px solid var(--border-2);\n border-radius: 8px;\n background: var(--surface);\n box-shadow: 0 8px 26px rgba(0, 0, 0, 0.2);\n font-size: 12.5px;\n color: var(--text);\n}\n.csv-pop__text {\n max-height: 320px;\n overflow: auto;\n overscroll-behavior: contain;\n white-space: pre-wrap;\n word-break: break-word;\n line-height: 1.5;\n user-select: text;\n}\n.csv-pop__foot {\n display: flex;\n justify-content: flex-end;\n}\n.csv-pop__copy {\n font-family: inherit;\n font-size: 12px;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n padding: 3px 12px;\n cursor: pointer;\n}\n.csv-pop__copy:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n</style>\n"],"mappings":"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"}
@@ -5,3 +5,5 @@ import t from "./DicomViewer.vue_vue_type_script_setup_true_lang.js";
5
5
  var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-36f5a4c5"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
+
9
+ //# sourceMappingURL=DicomViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DicomViewer.js","names":[],"sources":["../../src/components/DicomViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { DicomError, readDicom, type DicomSlice, type ParsedDicom } from '@/lib/dicom'\nimport {\n decodeDicomFrame,\n transferSyntaxName,\n UnsupportedTransferSyntaxError,\n} from '@/lib/dicomCodecs'\nimport {\n autoWindow,\n buildFramePixels,\n renderToImageData,\n type FramePixels,\n} from '@/lib/dicomRender'\nimport { formatBytes } from '@/lib/format'\n\n/**\n * The side viewer's DICOM (.dcm) mode: fetch a medical image's bytes, parse + decode it entirely in-browser\n * (see lib/dicom.ts → lib/dicomCodecs.ts → lib/dicomRender.ts), and draw it to a canvas with the controls a\n * basic medical viewer needs:\n * - window/level (brightness/contrast): left-drag on the image — horizontal = width, vertical = center.\n * - frames: a slider for multi-frame series (each frame decoded on demand).\n * - invert / zoom (wheel or buttons) / fit / reset.\n * Grayscale (CT/MR/X-ray — MONOCHROME1/2) is the primary path; RGB and palette-colour images render too.\n * The bytes come from the same download endpoint as any other file and never leave the browser.\n *\n * Two frame sources, unified behind {@link framePixelsAt}:\n * • single file (`url`) — frames come from that one file (multi-frame DICOM, e.g. enhanced CT / US cine);\n * • series stack (`slices`) — a list of independent single-frame DICOM files browsed as one scrollable\n * series (the classic \"one folder = one series, one file per slice\" hospital export). The wheel/cine\n * then steps through *files*. Decoded frames are cached + background-prefetched either way.\n */\nconst props = defineProps<{\n url?: string\n name: string\n /** Single-file: that file's size. Stack: the whole series' total bytes (just for the info strip). */\n size?: number\n /** Series-stack mode: independent DICOM files browsed as one series. Takes precedence over `url`. */\n slices?: DicomSlice[]\n /** Initial slice index in stack mode (the file the user actually clicked). */\n start?: number\n}>()\n\n// Stack mode is active when given more than one slice; a lone slice falls back to plain single-file display.\nconst stackMode = computed(() => (props.slices?.length ?? 0) > 1)\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\n\nconst parsed = ref<ParsedDicom | null>(null)\nconst framePixels = ref<FramePixels | null>(null)\nconst frameIndex = ref(0)\nconst decoding = ref(false)\n\n// Cine playback (multi-frame): decoded frames are cached so scrubbing/auto-play is instant after the first\n// pass, and a background prefetch warms the cache so even the very first loop is smooth.\nconst frameCache = new Map<number, FramePixels>()\nconst playing = ref(false)\nconst cineFps = ref(5)\nlet playTimer = 0\nlet prefetchToken = 0\n\nconst windowCenter = ref(0)\nconst windowWidth = ref(1)\nconst invert = ref(false)\nconst zoom = ref(1)\n// Initial window (from the file or auto) so \"reset\" can restore it after the user drags.\nlet initialCenter = 0\nlet initialWidth = 1\n\nconst canvasRef = ref<HTMLCanvasElement | null>(null)\nconst stageRef = ref<HTMLDivElement | null>(null)\nlet fitScale = 1\n\nconst meta = computed(() => parsed.value?.meta ?? null)\n// Full metadata table (patient/study/series/…), shown on demand via the toolbar toggle. Off by default —\n// it can include patient demographics (PHI), so the user opts in per file.\nconst showMeta = ref(false)\nconst tagGroups = computed(() => parsed.value?.tags ?? [])\nconst isGray = computed(() => framePixels.value?.mode === 'gray')\nconst totalFrames = computed(() =>\n stackMode.value ? props.slices!.length : (meta.value?.numberOfFrames ?? 1),\n)\nconst tsName = computed(() => (meta.value ? transferSyntaxName(meta.value.transferSyntaxUid) : ''))\n\nasync function loadFile(): Promise<void> {\n status.value = 'loading'\n errorMsg.value = ''\n parsed.value = null\n framePixels.value = null\n zoom.value = 1\n invert.value = false\n showMeta.value = false\n stopPlay()\n frameCache.clear()\n prefetchToken++ // abort any prefetch still running for the previous file\n // Stack mode opens on the slice the user clicked; single-file always starts at frame 0.\n const startAt =\n stackMode.value && props.start != null\n ? Math.min(Math.max(0, props.start), props.slices!.length - 1)\n : 0\n frameIndex.value = startAt\n try {\n if (!stackMode.value) {\n const res = await fetch(props.url ?? '')\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n parsed.value = readDicom(await res.arrayBuffer())\n }\n // decodeFrame owns the ready/error transition (and waits for the canvas to mount before painting). In\n // stack mode it parses slice `startAt` itself and establishes the representative meta from it.\n if (await decodeFrame(startAt, true)) void prefetchFrames()\n } catch (e) {\n fail(e)\n }\n}\n\nfunction fail(e: unknown): void {\n errorMsg.value =\n e instanceof DicomError || e instanceof UnsupportedTransferSyntaxError || e instanceof Error\n ? e.message\n : String(e)\n status.value = 'error'\n}\n\n// Decode + display one frame. `resetWindow` re-initialises the window from the file/auto (first load and\n// reset); frame navigation keeps the user's current window.\n// Produce the pixels for index `i` from whichever frame source is active:\n// • stack mode → slice `i` is its own DICOM file: parse it and decode its single frame. The first slice\n// to be decoded also establishes the representative meta/tags shown in the info strip + info panel.\n// • single file → frame `i` of the one already-parsed file.\nasync function framePixelsAt(i: number): Promise<FramePixels> {\n if (stackMode.value) {\n const pd = readDicom(await props.slices![i].load())\n parsed.value ??= pd\n return buildFramePixels(await decodeDicomFrame(pd.meta, pd.frameBytes(0)), pd.meta)\n }\n const p = parsed.value!\n return buildFramePixels(await decodeDicomFrame(p.meta, p.frameBytes(i)), p.meta)\n}\n\nasync function decodeFrame(i: number, resetWindow: boolean): Promise<boolean> {\n if (!stackMode.value && !parsed.value) return false\n let fp = frameCache.get(i)\n if (!fp) {\n decoding.value = true\n try {\n fp = await framePixelsAt(i)\n frameCache.set(i, fp)\n } catch (e) {\n fail(e)\n return false\n } finally {\n decoding.value = false\n }\n }\n // A newer frame was requested while this one was decoding (fast scroll / cine) — drop the stale result.\n if (i !== frameIndex.value) return false\n framePixels.value = fp\n const m = parsed.value?.meta\n if (resetWindow && fp.mode === 'gray' && m) {\n const fileHasWindow = m.windowCenter != null && m.windowWidth != null && m.windowWidth > 0\n const w = fileHasWindow\n ? { center: m.windowCenter as number, width: m.windowWidth as number }\n : autoWindow(fp)\n windowCenter.value = w.center\n windowWidth.value = w.width\n initialCenter = w.center\n initialWidth = w.width\n }\n // First successful frame: flip to 'ready' so the <canvas> mounts, then wait a tick before the first paint\n // (otherwise draw() runs against an unmounted canvas and no-ops, leaving it black). Later frames just draw.\n const firstPaint = status.value !== 'ready'\n status.value = 'ready'\n if (firstPaint) {\n await nextTick()\n computeFit()\n }\n draw()\n return true\n}\n\n// Background-decode the remaining frames into the cache so cine playback and scrubbing are smooth from the\n// first loop. Yields between frames to keep the UI responsive; aborts if the file changes (token bump).\nasync function prefetchFrames(): Promise<void> {\n const n = totalFrames.value\n if (n <= 1 || (!stackMode.value && !parsed.value)) return\n const token = ++prefetchToken\n for (let i = 0; i < n; i++) {\n if (token !== prefetchToken) return // file changed — stop\n if (!frameCache.has(i)) {\n try {\n const fp = await framePixelsAt(i)\n if (token !== prefetchToken) return\n frameCache.set(i, fp)\n } catch {\n // Ignore — navigating to this frame on demand will surface the error if there is one.\n }\n }\n await new Promise((r) => setTimeout(r, 0)) // yield to the event loop between frames\n }\n}\n\n// --- drawing -------------------------------------------------------------------------------------------\nlet rafId = 0\nfunction scheduleDraw(): void {\n if (rafId) return\n rafId = requestAnimationFrame(() => {\n rafId = 0\n draw()\n })\n}\n\nfunction draw(): void {\n const fp = framePixels.value\n const cv = canvasRef.value\n if (!fp || !cv) return\n if (cv.width !== fp.width) cv.width = fp.width\n if (cv.height !== fp.height) cv.height = fp.height\n const ctx = cv.getContext('2d')\n if (!ctx) return\n const img = renderToImageData(fp, {\n windowCenter: windowCenter.value,\n windowWidth: windowWidth.value,\n invert: invert.value,\n })\n ctx.putImageData(img, 0, 0)\n applyDisplaySize()\n}\n\nfunction computeFit(): void {\n const st = stageRef.value\n const fp = framePixels.value\n if (!st || !fp) return\n const cw = Math.max(1, st.clientWidth - 24)\n const ch = Math.max(1, st.clientHeight - 24)\n const s = Math.min(cw / fp.width, ch / fp.height)\n fitScale = Number.isFinite(s) && s > 0 ? s : 1\n}\n\nfunction applyDisplaySize(): void {\n const cv = canvasRef.value\n const fp = framePixels.value\n if (!cv || !fp) return\n const scale = fitScale * zoom.value\n cv.style.width = `${Math.max(1, Math.round(fp.width * scale))}px`\n cv.style.height = `${Math.max(1, Math.round(fp.height * scale))}px`\n}\n\n// --- interaction ---------------------------------------------------------------------------------------\n// Left-drag = window/level (radiology convention: horizontal → width/contrast, vertical → center/brightness).\n// Disabled for RGB/palette images (no VOI window applies).\nfunction onPointerDown(e: PointerEvent): void {\n if (e.button !== 0 || !isGray.value) return\n e.preventDefault()\n const el = e.currentTarget as HTMLElement\n el.setPointerCapture(e.pointerId)\n const startX = e.clientX\n const startY = e.clientY\n const startWW = windowWidth.value\n const startWC = windowCenter.value\n const fp = framePixels.value\n const range = fp ? fp.maxValue - fp.minValue || 255 : 255\n const k = Math.max(1, range / 512)\n const move = (ev: PointerEvent) => {\n windowWidth.value = Math.max(1, startWW + (ev.clientX - startX) * k)\n windowCenter.value = startWC + (ev.clientY - startY) * k\n scheduleDraw()\n }\n const up = (ev: PointerEvent) => {\n el.releasePointerCapture(ev.pointerId)\n el.removeEventListener('pointermove', move)\n el.removeEventListener('pointerup', up)\n }\n el.addEventListener('pointermove', move)\n el.addEventListener('pointerup', up)\n}\n\nfunction onWheel(e: WheelEvent): void {\n e.preventDefault()\n // Multi-frame DICOM: the wheel scrubs through slices like a hospital viewer (hold Ctrl/Shift to zoom).\n // Single-frame: nothing to scrub, so the wheel zooms as before.\n if (totalFrames.value > 1 && !e.ctrlKey && !e.shiftKey) {\n stopPlay() // a manual scrub takes over from auto-play\n stepFrame(e.deltaY > 0 ? 1 : -1)\n return\n }\n const factor = e.deltaY < 0 ? 1.12 : 1 / 1.12\n zoom.value = Math.min(20, Math.max(0.1, zoom.value * factor))\n applyDisplaySize()\n}\n\n// --- cine (multi-frame playback) -----------------------------------------------------------------------\n// Wheel/buttons clamp at the ends; auto-play wraps so it loops like a cine clip.\nfunction stepFrame(delta: number): void {\n const n = totalFrames.value\n if (n <= 1) return\n frameIndex.value = Math.min(n - 1, Math.max(0, frameIndex.value + delta))\n}\n\nfunction togglePlay(): void {\n if (playing.value) stopPlay()\n else startPlay()\n}\nfunction startPlay(): void {\n if (totalFrames.value <= 1 || playing.value) return\n playing.value = true\n const tick = () => {\n if (!playing.value) return\n const n = totalFrames.value\n frameIndex.value = (frameIndex.value + 1) % n // wrap → loop\n playTimer = window.setTimeout(tick, Math.max(20, Math.round(1000 / cineFps.value)))\n }\n tick()\n}\nfunction stopPlay(): void {\n playing.value = false\n if (playTimer) {\n clearTimeout(playTimer)\n playTimer = 0\n }\n}\n\nfunction zoomBy(factor: number): void {\n zoom.value = Math.min(20, Math.max(0.1, zoom.value * factor))\n applyDisplaySize()\n}\n\nfunction fitToStage(): void {\n zoom.value = 1\n computeFit()\n applyDisplaySize()\n}\n\nfunction resetView(): void {\n zoom.value = 1\n invert.value = false\n if (framePixels.value?.mode === 'gray') {\n windowCenter.value = initialCenter\n windowWidth.value = initialWidth\n }\n computeFit()\n draw()\n}\n\nwatch(frameIndex, (i) => {\n if (status.value === 'ready') void decodeFrame(i, false)\n})\nwatch(invert, scheduleDraw)\n\n// Re-fit when the panel is resized (the splitter or window changes the stage size).\nlet ro: ResizeObserver | null = null\nonMounted(() => {\n void loadFile()\n if (typeof ResizeObserver !== 'undefined' && stageRef.value) {\n ro = new ResizeObserver(() => {\n computeFit()\n applyDisplaySize()\n })\n ro.observe(stageRef.value)\n }\n})\n// Reload when the source changes — a new single-file URL, or a new series stack (identity changes when the\n// browser rebuilds the slice list for a different folder / clicked file).\nwatch([() => props.url, () => props.slices], loadFile)\nonBeforeUnmount(() => {\n stopPlay()\n prefetchToken++ // stop any in-flight prefetch\n if (rafId) cancelAnimationFrame(rafId)\n ro?.disconnect()\n})\n\nconst wcLabel = computed(() => Math.round(windowCenter.value))\nconst wwLabel = computed(() => Math.round(windowWidth.value))\nconst zoomLabel = computed(() => `${Math.round(fitScale * zoom.value * 100)}%`)\n</script>\n\n<template>\n <div class=\"dv\">\n <p v-if=\"status === 'loading'\" class=\"dv__msg\">加载并解码 DICOM…</p>\n\n <div v-else-if=\"status === 'error'\" class=\"dv__msg dv__msg--error\">\n <p>{{ errorMsg }}</p>\n <a class=\"dv__dl\" :href=\"url\" :download=\"name\">下载原文件</a>\n <button type=\"button\" class=\"dv__retry\" @click=\"loadFile\">重试</button>\n </div>\n\n <template v-else>\n <!-- toolbar -->\n <div class=\"dv__bar\">\n <div class=\"dv__group\">\n <button type=\"button\" class=\"dv__btn\" title=\"缩小\" @click=\"zoomBy(1 / 1.2)\">−</button>\n <span class=\"dv__zoom\">{{ zoomLabel }}</span>\n <button type=\"button\" class=\"dv__btn\" title=\"放大\" @click=\"zoomBy(1.2)\">+</button>\n <button type=\"button\" class=\"dv__btn\" title=\"适应窗口\" @click=\"fitToStage\">适应</button>\n </div>\n <div class=\"dv__group\">\n <button\n type=\"button\"\n class=\"dv__btn\"\n :class=\"{ 'dv__btn--on': invert }\"\n :disabled=\"!isGray\"\n title=\"反相显示\"\n @click=\"invert = !invert\"\n >\n 反相\n </button>\n <button type=\"button\" class=\"dv__btn\" title=\"重置窗位/缩放\" @click=\"resetView\">\n 重置\n </button>\n <button\n type=\"button\"\n class=\"dv__btn\"\n :class=\"{ 'dv__btn--on': showMeta }\"\n :disabled=\"!tagGroups.length\"\n title=\"DICOM 元数据\"\n @click=\"showMeta = !showMeta\"\n >\n 信息\n </button>\n </div>\n </div>\n\n <!-- image canvas -->\n <div ref=\"stageRef\" class=\"dv__stage\" @wheel=\"onWheel\">\n <canvas\n ref=\"canvasRef\"\n class=\"dv__canvas\"\n :class=\"{ 'dv__canvas--wl': isGray }\"\n @pointerdown=\"onPointerDown\"\n />\n <span v-if=\"decoding\" class=\"dv__decoding\">解码中…</span>\n\n <!-- DICOM metadata overlay (toggled by the toolbar's 信息 button). Covers the image while open;\n scrolls on its own — @wheel.stop keeps the wheel from zooming the image underneath. -->\n <div v-if=\"showMeta\" class=\"dv__meta\" @wheel.stop>\n <div class=\"dv__meta-head\">\n <span>DICOM 元数据</span>\n <button type=\"button\" class=\"dv__meta-close\" title=\"关闭\" @click=\"showMeta = false\">\n ✕\n </button>\n </div>\n <div v-for=\"g in tagGroups\" :key=\"g.title\" class=\"dv__meta-group\">\n <h4 class=\"dv__meta-title\">{{ g.title }}</h4>\n <div v-for=\"r in g.rows\" :key=\"r.label\" class=\"dv__meta-row\">\n <span class=\"dv__meta-label\">{{ r.label }}</span>\n <span class=\"dv__meta-val\">{{ r.value }}</span>\n </div>\n </div>\n <p v-if=\"!tagGroups.length\" class=\"dv__meta-empty\">无可显示的元数据</p>\n </div>\n </div>\n\n <!-- multi-frame: cine playback + scrub slider (wheel over the image also scrubs) -->\n <div v-if=\"totalFrames > 1\" class=\"dv__frames\">\n <button\n type=\"button\"\n class=\"dv__btn\"\n :class=\"{ 'dv__btn--on': playing }\"\n :title=\"playing ? '暂停' : '播放(循环)'\"\n @click=\"togglePlay\"\n >\n {{ playing ? '⏸' : '▶' }}\n </button>\n <select v-model.number=\"cineFps\" class=\"dv__fps\" title=\"播放速度(帧/秒)\">\n <option :value=\"5\">5 fps</option>\n <option :value=\"10\">10 fps</option>\n <option :value=\"15\">15 fps</option>\n <option :value=\"24\">24 fps</option>\n </select>\n <button\n type=\"button\"\n class=\"dv__btn\"\n :disabled=\"frameIndex <= 0\"\n @click=\"(stopPlay(), (frameIndex = Math.max(0, frameIndex - 1)))\"\n >\n ‹\n </button>\n <input\n v-model.number=\"frameIndex\"\n type=\"range\"\n class=\"dv__range\"\n min=\"0\"\n :max=\"totalFrames - 1\"\n step=\"1\"\n @pointerdown=\"stopPlay\"\n />\n <button\n type=\"button\"\n class=\"dv__btn\"\n :disabled=\"frameIndex >= totalFrames - 1\"\n @click=\"(stopPlay(), (frameIndex = Math.min(totalFrames - 1, frameIndex + 1)))\"\n >\n ›\n </button>\n <span class=\"dv__frameno\">{{ frameIndex + 1 }} / {{ totalFrames }}</span>\n <span class=\"dv__hint\">滚轮切换层面</span>\n </div>\n\n <!-- metadata strip -->\n <div v-if=\"meta\" class=\"dv__info\">\n <span v-if=\"meta.modality\" class=\"dv__tag\">{{ meta.modality }}</span>\n <span v-if=\"stackMode\" class=\"dv__series\">序列 · {{ totalFrames }} 层</span>\n <span>{{ meta.columns }}×{{ meta.rows }}</span>\n <span>{{ meta.bitsStored }}/{{ meta.bitsAllocated }}-bit</span>\n <span>{{ meta.photometric }}</span>\n <span v-if=\"isGray\" class=\"dv__wl\">窗位 C {{ wcLabel }} · W {{ wwLabel }}</span>\n <span class=\"dv__ts\">{{ tsName }}</span>\n <span v-if=\"props.size\">{{ formatBytes(props.size) }}</span>\n <span v-if=\"meta.seriesDescription\" class=\"dv__desc\" :title=\"meta.seriesDescription\">{{\n meta.seriesDescription\n }}</span>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.dv {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.dv__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dv__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dv__dl,\n.dv__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n text-decoration: none;\n}\n.dv__dl:hover,\n.dv__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* Toolbar --------------------------------------------------------------------------------------------- */\n.dv__bar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 10px;\n padding: 8px 12px;\n border-bottom: 1px solid var(--border);\n}\n.dv__group {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n.dv__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 30px;\n height: 28px;\n padding: 0 9px;\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n color: var(--muted);\n font-family: inherit;\n font-size: 12.5px;\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s,\n border-color 0.15s;\n}\n.dv__btn:hover:not(:disabled) {\n border-color: var(--accent);\n}\n.dv__btn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.dv__btn:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.dv__zoom {\n min-width: 46px;\n text-align: center;\n font-size: 11.5px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n\n/* Image stage --------------------------------------------------------------------------------------- */\n.dv__stage {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 12px;\n /* a neutral dark mat so grayscale medical images read with proper contrast */\n background: repeating-conic-gradient(#1a1a1a 0% 25%, #202020 0% 50%) 50% / 22px 22px;\n}\n.dv__canvas {\n display: block;\n margin: auto;\n image-rendering: auto;\n background: #000;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);\n}\n.dv__canvas--wl {\n cursor: crosshair;\n touch-action: none;\n}\n.dv__decoding {\n position: absolute;\n top: 10px;\n right: 12px;\n padding: 3px 9px;\n border-radius: 999px;\n background: rgba(0, 0, 0, 0.6);\n color: #fff;\n font-size: 11px;\n}\n\n/* Metadata overlay --------------------------------------------------------------------------------- */\n.dv__meta {\n position: absolute;\n inset: 0;\n z-index: 3;\n overflow-y: auto;\n overscroll-behavior: contain;\n padding: 10px 14px 18px;\n background: color-mix(in srgb, var(--surface) 94%, transparent);\n backdrop-filter: blur(2px);\n}\n.dv__meta-head {\n position: sticky;\n top: 0;\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin: -10px -14px 8px;\n padding: 9px 14px;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n font-weight: 600;\n color: var(--text-strong);\n}\n.dv__meta-close {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--faint);\n font-size: 13px;\n cursor: pointer;\n}\n.dv__meta-close:hover {\n color: var(--accent);\n background: var(--surface-2);\n}\n.dv__meta-group {\n margin-top: 12px;\n}\n.dv__meta-title {\n margin: 0 0 4px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--accent);\n}\n.dv__meta-row {\n display: flex;\n gap: 10px;\n padding: 3px 0;\n border-bottom: 1px solid var(--border);\n font-size: 12px;\n line-height: 1.5;\n}\n.dv__meta-label {\n flex: 0 0 38%;\n color: var(--faint);\n}\n.dv__meta-val {\n flex: 1 1 auto;\n min-width: 0;\n color: var(--text);\n word-break: break-word;\n font-variant-numeric: tabular-nums;\n}\n.dv__meta-empty {\n margin-top: 16px;\n font-size: 12.5px;\n color: var(--faint);\n}\n\n/* Frame slider -------------------------------------------------------------------------------------- */\n.dv__frames {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n border-top: 1px solid var(--border);\n}\n.dv__range {\n flex: 1 1 auto;\n min-width: 0;\n accent-color: var(--accent);\n}\n.dv__fps {\n flex-shrink: 0;\n height: 28px;\n padding: 0 4px;\n background: var(--surface-2);\n border: 1px solid var(--border);\n border-radius: 7px;\n color: var(--muted);\n font-family: inherit;\n font-size: 11.5px;\n cursor: pointer;\n}\n.dv__frameno {\n flex-shrink: 0;\n font-size: 11.5px;\n color: var(--faint);\n font-variant-numeric: tabular-nums;\n}\n.dv__hint {\n flex-shrink: 0;\n font-size: 11px;\n color: var(--faint);\n opacity: 0.8;\n}\n\n/* Metadata strip ------------------------------------------------------------------------------------ */\n.dv__info {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px 12px;\n padding: 9px 14px;\n border-top: 1px solid var(--border);\n font-size: 11.5px;\n color: var(--faint);\n}\n.dv__tag {\n padding: 1px 8px;\n border-radius: 999px;\n background: var(--accent);\n color: #fff;\n font-weight: 600;\n}\n.dv__series {\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--accent);\n color: var(--accent);\n font-weight: 600;\n}\n.dv__wl {\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n}\n.dv__ts {\n color: var(--muted);\n}\n.dv__desc {\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n</style>\n"],"mappings":""}
@@ -360,3 +360,5 @@ var ce = { class: "dv" }, le = {
360
360
  });
361
361
  //#endregion
362
362
  export { x as default };
363
+
364
+ //# sourceMappingURL=DicomViewer.vue_vue_type_script_setup_true_lang.js.map