@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.
- package/README.md +1 -0
- package/dist/FileViewer.js +2 -0
- package/dist/FileViewer.js.map +1 -0
- package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/FileViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/AppxViewer.js +2 -0
- package/dist/components/AppxViewer.js.map +1 -0
- package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/AppxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ArchiveBrowser.js +2 -0
- package/dist/components/ArchiveBrowser.js.map +1 -0
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ArchiveBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/AudioViewer.js +2 -0
- package/dist/components/AudioViewer.js.map +1 -0
- package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/AudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/BinObjBrowser.js +2 -0
- package/dist/components/BinObjBrowser.js.map +1 -0
- package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/BinObjBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CertViewer.js +2 -0
- package/dist/components/CertViewer.js.map +1 -0
- package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ClassViewer.js +2 -0
- package/dist/components/ClassViewer.js.map +1 -0
- package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ClassViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CodeBlock.js +2 -0
- package/dist/components/CodeBlock.js.map +1 -0
- package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CodeView.js +2 -0
- package/dist/components/CodeView.js.map +1 -0
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CodeView.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/CsvViewer.js +2 -0
- package/dist/components/CsvViewer.js.map +1 -0
- package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/CsvViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DicomViewer.js +2 -0
- package/dist/components/DicomViewer.js.map +1 -0
- package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DicomViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DocxNavigationPane.js +2 -0
- package/dist/components/DocxNavigationPane.js.map +1 -0
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DocxViewer.js +2 -0
- package/dist/components/DocxViewer.js.map +1 -0
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DrawioViewer.js +2 -0
- package/dist/components/DrawioViewer.js.map +1 -0
- package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DrawioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/DxfViewer.js +2 -0
- package/dist/components/DxfViewer.js.map +1 -0
- package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/DxfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/EbookViewer.js +2 -0
- package/dist/components/EbookViewer.js.map +1 -0
- package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/EbookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/EmlViewer.js +2 -0
- package/dist/components/EmlViewer.js.map +1 -0
- package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/EmlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/EplViewer.js +2 -0
- package/dist/components/EplViewer.js.map +1 -0
- package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FileViewerBody.js +2 -0
- package/dist/components/FileViewerBody.js.map +1 -0
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FileViewerPanel.js +2 -0
- package/dist/components/FileViewerPanel.js.map +1 -0
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FontViewer.js +2 -0
- package/dist/components/FontViewer.js.map +1 -0
- package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FontViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/FrontmatterCard.js +2 -0
- package/dist/components/FrontmatterCard.js.map +1 -0
- package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/FrontmatterCard.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/GeoViewer.js +2 -0
- package/dist/components/GeoViewer.js.map +1 -0
- package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/GeoViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/HarViewer.js +2 -0
- package/dist/components/HarViewer.js.map +1 -0
- package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/HexViewer.js +2 -0
- package/dist/components/HexViewer.js.map +1 -0
- package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/HexViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/HprofViewer.js +2 -0
- package/dist/components/HprofViewer.js.map +1 -0
- package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/HprofViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ImageLightbox.js +2 -0
- package/dist/components/ImageLightbox.js.map +1 -0
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ImageLightbox.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/InlineText.js +2 -0
- package/dist/components/InlineText.js.map +1 -0
- package/dist/components/JksViewer.js +2 -0
- package/dist/components/JksViewer.js.map +1 -0
- package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/JsonlViewer.js +2 -0
- package/dist/components/JsonlViewer.js.map +1 -0
- package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/JsonlViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/LogViewer.js +2 -0
- package/dist/components/LogViewer.js.map +1 -0
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/LogViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/LrcViewer.js +2 -0
- package/dist/components/LrcViewer.js.map +1 -0
- package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/LrcViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MarkdownTable.js +2 -0
- package/dist/components/MarkdownTable.js.map +1 -0
- package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MarkdownTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MarkdownText.js +2 -0
- package/dist/components/MarkdownText.js.map +1 -0
- package/dist/components/MhtViewer.js +2 -0
- package/dist/components/MhtViewer.js.map +1 -0
- package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MhtViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MindMapView.js +2 -0
- package/dist/components/MindMapView.js.map +1 -0
- package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MindMapView.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MissingDependency.js +2 -0
- package/dist/components/MissingDependency.js.map +1 -0
- package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MissingDependency.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ModelViewer.js +2 -0
- package/dist/components/ModelViewer.js.map +1 -0
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/MsiViewer.js +2 -0
- package/dist/components/MsiViewer.js.map +1 -0
- package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/MsiViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/NotebookViewer.js +2 -0
- package/dist/components/NotebookViewer.js.map +1 -0
- package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/NotebookViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PdfViewer.js +2 -0
- package/dist/components/PdfViewer.js.map +1 -0
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PptxNavigationPane.js +2 -0
- package/dist/components/PptxNavigationPane.js.map +1 -0
- package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PptxViewer.js +3 -1
- package/dist/components/PptxViewer.js.map +1 -0
- package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js +55 -49
- package/dist/components/PptxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/PsdViewer.js +2 -0
- package/dist/components/PsdViewer.js.map +1 -0
- package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/QmcAudioViewer.js +2 -0
- package/dist/components/QmcAudioViewer.js.map +1 -0
- package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/QmcAudioViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/RasterViewer.js +2 -0
- package/dist/components/RasterViewer.js.map +1 -0
- package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/RasterViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/RtfViewer.js +2 -0
- package/dist/components/RtfViewer.js.map +1 -0
- package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/RtfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/SevenZipBrowser.js +2 -0
- package/dist/components/SevenZipBrowser.js.map +1 -0
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/SevenZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/SqliteViewer.js +2 -0
- package/dist/components/SqliteViewer.js.map +1 -0
- package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/SubtitleViewer.js +2 -0
- package/dist/components/SubtitleViewer.js.map +1 -0
- package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/SubtitleViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxChart.js +2 -0
- package/dist/components/XlsxChart.js.map +1 -0
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XlsxViewer.js +2 -0
- package/dist/components/XlsxViewer.js.map +1 -0
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/XmindViewer.js +2 -0
- package/dist/components/XmindViewer.js.map +1 -0
- package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/XmindViewer.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ZipBrowser.js +2 -0
- package/dist/components/ZipBrowser.js.map +1 -0
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ZipBrowser.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/ZoomImage.js +2 -0
- package/dist/components/ZoomImage.js.map +1 -0
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/ZoomImage.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplCodeBlock.js +2 -0
- package/dist/components/epl/EplCodeBlock.js.map +1 -0
- package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplCodeBlock.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplDoc.js +2 -0
- package/dist/components/epl/EplDoc.js.map +1 -0
- package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplDoc.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplMethod.js +2 -0
- package/dist/components/epl/EplMethod.js.map +1 -0
- package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplMethod.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplResource.js +2 -0
- package/dist/components/epl/EplResource.js.map +1 -0
- package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplTree.js +2 -0
- package/dist/components/epl/EplTree.js.map +1 -0
- package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplTree.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/EplVarTable.js +2 -0
- package/dist/components/epl/EplVarTable.js.map +1 -0
- package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/epl/EplVarTable.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/epl/numbering.js +2 -0
- package/dist/components/epl/numbering.js.map +1 -0
- package/dist/components/icons/Icon.js +2 -0
- package/dist/components/icons/Icon.js.map +1 -0
- package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js +2 -0
- package/dist/components/icons/Icon.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/icons/iconPaths.js +2 -0
- package/dist/components/icons/iconPaths.js.map +1 -0
- package/dist/components/optionalViewer.js +2 -0
- package/dist/components/optionalViewer.js.map +1 -0
- package/dist/components/ui/ClampBlock.js +2 -0
- package/dist/components/ui/ClampBlock.js.map +1 -0
- package/dist/composables/useElementNavigation.js +2 -0
- package/dist/composables/useElementNavigation.js.map +1 -0
- package/dist/composables/useFileViewer.js +2 -0
- package/dist/composables/useFileViewer.js.map +1 -0
- package/dist/composables/useLightbox.js +2 -0
- package/dist/composables/useLightbox.js.map +1 -0
- package/dist/composables/useLogRules.js +2 -0
- package/dist/composables/useLogRules.js.map +1 -0
- package/dist/lib/aes.js +2 -0
- package/dist/lib/aes.js.map +1 -0
- package/dist/lib/appx.js +2 -0
- package/dist/lib/appx.js.map +1 -0
- package/dist/lib/archiveHtml.js +2 -0
- package/dist/lib/archiveHtml.js.map +1 -0
- package/dist/lib/asn1.js +2 -0
- package/dist/lib/asn1.js.map +1 -0
- package/dist/lib/binobj.js +2 -0
- package/dist/lib/binobj.js.map +1 -0
- package/dist/lib/brotli.js +2 -0
- package/dist/lib/brotli.js.map +1 -0
- package/dist/lib/bzip2.js +2 -0
- package/dist/lib/bzip2.js.map +1 -0
- package/dist/lib/classfile.js +2 -0
- package/dist/lib/classfile.js.map +1 -0
- package/dist/lib/codePreview.js +2 -0
- package/dist/lib/codePreview.js.map +1 -0
- package/dist/lib/codefold.js +2 -0
- package/dist/lib/codefold.js.map +1 -0
- package/dist/lib/codeformat.js +2 -0
- package/dist/lib/codeformat.js.map +1 -0
- package/dist/lib/csv.js +2 -0
- package/dist/lib/csv.js.map +1 -0
- package/dist/lib/des.js +2 -0
- package/dist/lib/des.js.map +1 -0
- package/dist/lib/diagram.js +2 -0
- package/dist/lib/diagram.js.map +1 -0
- package/dist/lib/dicom.js +2 -0
- package/dist/lib/dicom.js.map +1 -0
- package/dist/lib/dicomCodecs.js +2 -0
- package/dist/lib/dicomCodecs.js.map +1 -0
- package/dist/lib/dicomRender.js +2 -0
- package/dist/lib/dicomRender.js.map +1 -0
- package/dist/lib/docxNavigation.js +2 -0
- package/dist/lib/docxNavigation.js.map +1 -0
- package/dist/lib/dxf.js +2 -0
- package/dist/lib/dxf.js.map +1 -0
- package/dist/lib/eml.js +2 -0
- package/dist/lib/eml.js.map +1 -0
- package/dist/lib/encoding.js +2 -0
- package/dist/lib/encoding.js.map +1 -0
- package/dist/lib/epl/code/codeDataParser.js +2 -0
- package/dist/lib/epl/code/codeDataParser.js.map +1 -0
- package/dist/lib/epl/code/codeLines.js +2 -0
- package/dist/lib/epl/code/codeLines.js.map +1 -0
- package/dist/lib/epl/code/textCode.js +2 -0
- package/dist/lib/epl/code/textCode.js.map +1 -0
- package/dist/lib/epl/eplDocument.js +2 -0
- package/dist/lib/epl/eplDocument.js.map +1 -0
- package/dist/lib/epl/imageList.js +2 -0
- package/dist/lib/epl/imageList.js.map +1 -0
- package/dist/lib/epl/index.js +2 -0
- package/dist/lib/epl/index.js.map +1 -0
- package/dist/lib/epl/io/binaryReader.js +2 -0
- package/dist/lib/epl/io/binaryReader.js.map +1 -0
- package/dist/lib/epl/io/encoding.js +2 -0
- package/dist/lib/epl/io/encoding.js.map +1 -0
- package/dist/lib/epl/model/eplSystemId.js +2 -0
- package/dist/lib/epl/model/eplSystemId.js.map +1 -0
- package/dist/lib/epl/names/eplLibData.js +2 -0
- package/dist/lib/epl/names/eplLibData.js.map +1 -0
- package/dist/lib/epl/names/idToNameMap.js +2 -0
- package/dist/lib/epl/names/idToNameMap.js.map +1 -0
- package/dist/lib/epl/names/libNameProvider.js +2 -0
- package/dist/lib/epl/names/libNameProvider.js.map +1 -0
- package/dist/lib/epl/program.js +2 -0
- package/dist/lib/epl/program.js.map +1 -0
- package/dist/lib/epl/projectFileReader.js +2 -0
- package/dist/lib/epl/projectFileReader.js.map +1 -0
- package/dist/lib/fb2.js +2 -0
- package/dist/lib/fb2.js.map +1 -0
- package/dist/lib/fileType.js +2 -0
- package/dist/lib/fileType.js.map +1 -0
- package/dist/lib/fonts.js +2 -0
- package/dist/lib/fonts.js.map +1 -0
- package/dist/lib/format.js +2 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/frontmatter.js +2 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/geo.js +2 -0
- package/dist/lib/geo.js.map +1 -0
- package/dist/lib/har.js +2 -0
- package/dist/lib/har.js.map +1 -0
- package/dist/lib/highlight.js +2 -0
- package/dist/lib/highlight.js.map +1 -0
- package/dist/lib/highlightLangs.js +2 -0
- package/dist/lib/highlightLangs.js.map +1 -0
- package/dist/lib/hprof.js +2 -0
- package/dist/lib/hprof.js.map +1 -0
- package/dist/lib/ipynb.js +2 -0
- package/dist/lib/ipynb.js.map +1 -0
- package/dist/lib/jks.js +2 -0
- package/dist/lib/jks.js.map +1 -0
- package/dist/lib/jsonl.js +2 -0
- package/dist/lib/jsonl.js.map +1 -0
- package/dist/lib/lrc.js +2 -0
- package/dist/lib/lrc.js.map +1 -0
- package/dist/lib/lzma.js +2 -0
- package/dist/lib/lzma.js.map +1 -0
- package/dist/lib/markdown/blocks.js +2 -0
- package/dist/lib/markdown/blocks.js.map +1 -0
- package/dist/lib/markdown/inline.js +2 -0
- package/dist/lib/markdown/inline.js.map +1 -0
- package/dist/lib/markdown/math.js +2 -0
- package/dist/lib/markdown/math.js.map +1 -0
- package/dist/lib/markdown/segmentize.js +2 -0
- package/dist/lib/markdown/segmentize.js.map +1 -0
- package/dist/lib/markdown/styles.js +2 -0
- package/dist/lib/markdown/styles.js.map +1 -0
- package/dist/lib/mht.js +2 -0
- package/dist/lib/mht.js.map +1 -0
- package/dist/lib/midi.js +2 -0
- package/dist/lib/midi.js.map +1 -0
- package/dist/lib/mime.js +2 -0
- package/dist/lib/mime.js.map +1 -0
- package/dist/lib/mindmap.js +2 -0
- package/dist/lib/mindmap.js.map +1 -0
- package/dist/lib/mobi.js +2 -0
- package/dist/lib/mobi.js.map +1 -0
- package/dist/lib/msi.js +2 -0
- package/dist/lib/msi.js.map +1 -0
- package/dist/lib/optionalDep.js +2 -0
- package/dist/lib/optionalDep.js.map +1 -0
- package/dist/lib/pkcs12.js +2 -0
- package/dist/lib/pkcs12.js.map +1 -0
- package/dist/lib/pptxNavigation.js +2 -0
- package/dist/lib/pptxNavigation.js.map +1 -0
- package/dist/lib/qmc.js +2 -0
- package/dist/lib/qmc.js.map +1 -0
- package/dist/lib/rar.js +2 -0
- package/dist/lib/rar.js.map +1 -0
- package/dist/lib/rawPreview.js +2 -0
- package/dist/lib/rawPreview.js.map +1 -0
- package/dist/lib/rc2.js +2 -0
- package/dist/lib/rc2.js.map +1 -0
- package/dist/lib/resolveInput.js +2 -0
- package/dist/lib/resolveInput.js.map +1 -0
- package/dist/lib/rtf.js +2 -0
- package/dist/lib/rtf.js.map +1 -0
- package/dist/lib/sevenZip.js +2 -0
- package/dist/lib/sevenZip.js.map +1 -0
- package/dist/lib/sha1.js +2 -0
- package/dist/lib/sha1.js.map +1 -0
- package/dist/lib/sha256.js +2 -0
- package/dist/lib/sha256.js.map +1 -0
- package/dist/lib/speechAudio.js +2 -0
- package/dist/lib/speechAudio.js.map +1 -0
- package/dist/lib/sqlite.js +2 -0
- package/dist/lib/sqlite.js.map +1 -0
- package/dist/lib/subtitle.js +2 -0
- package/dist/lib/subtitle.js.map +1 -0
- package/dist/lib/tarball.js +2 -0
- package/dist/lib/tarball.js.map +1 -0
- package/dist/lib/viewerFormats.js +2 -0
- package/dist/lib/viewerFormats.js.map +1 -0
- package/dist/lib/viewerKind.js +2 -0
- package/dist/lib/viewerKind.js.map +1 -0
- package/dist/lib/x509.js +2 -0
- package/dist/lib/x509.js.map +1 -0
- package/dist/lib/xlsx.js +2 -0
- package/dist/lib/xlsx.js.map +1 -0
- package/dist/lib/xlsxCharts.js +2 -0
- package/dist/lib/xlsxCharts.js.map +1 -0
- package/dist/lib/xmind.js +2 -0
- package/dist/lib/xmind.js.map +1 -0
- package/dist/lib/zip.js +2 -0
- package/dist/lib/zip.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EplViewer.js","names":[],"sources":["../../src/components/EplViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { decompileEpl, EplEncryptedError, type EplPreview } from '@/lib/epl'\nimport EplTree from './epl/EplTree.vue'\nimport EplDoc from './epl/EplDoc.vue'\nimport type { EplDocRef, EplDocKind } from './epl/docTypes'\n\n/**\n * IDE-style viewer for e-language (易语言) `.e` / `.ec` project files. Fetches the BYTES and parses + decompiles\n * them entirely in-browser via {@link decompileEpl} (port of OpenEpl/EProjectFile, see lib/epl) — no dependency,\n * like the eml / mht / binobj viewers. Lays the result out like the 易语言 IDE: a「程序」tree on the left\n * (程序集 / 类 + project tables), the selected document on the right (definitions as bordered tables, method\n * bodies with dashed block guides), open documents as bottom tabs, and 支持库/程序 tabs under the tree.\n *\n * Encrypted files surface a friendly message (decryption is a separate path); a large file is gated behind a\n * confirm (all parsing is in-memory). Entry shape mirrors the other self-fetching viewers: a real download URL,\n * or a blob URL minted by an archive browser from extracted bytes.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error' | 'encrypted'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst encryptTip = ref('')\nconst preview = ref<EplPreview | null>(null)\n\nconst SIZE_LIMIT = 32 * 1024 * 1024\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n encryptTip.value = ''\n preview.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n preview.value = decompileEpl(new Uint8Array(buf))\n status.value = 'ready'\n } catch (e) {\n if (e instanceof EplEncryptedError) {\n status.value = 'encrypted'\n encryptTip.value = e.tip ?? ''\n } else {\n status.value = 'error'\n errorMsg.value = e instanceof Error ? e.message : String(e)\n }\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\nwatch(() => props.url, load, { immediate: true })\n\nconst meta = computed(() => preview.value?.meta ?? null)\nconst program = computed(() => preview.value?.program ?? null)\nconst fileTypeLabel = computed(() => {\n const t = meta.value?.fileType\n return t === 'source' ? '源码 .e' : t === 'module' ? '模块 .ec' : '未知'\n})\n\n// ── IDE state: side tab + open document tabs ──────────────────────────────────────────────────────\nconst sideTab = ref<'program' | 'libs'>('program')\nconst openTabs = ref<EplDocRef[]>([])\nconst activeId = ref('')\nconst activeDoc = computed(() => openTabs.value.find((t) => t.id === activeId.value) ?? null)\n\nconst TABLE_TITLE: Record<Exclude<EplDocKind, 'class'>, string> = {\n globals: '全局变量',\n structs: '自定义数据类型',\n dll: 'Dll命令',\n constants: '常量表',\n resources: '图片/声音',\n forms: '资源表',\n libraries: '模块引用表',\n}\n\nfunction defaultDoc(p: NonNullable<typeof preview.value>['program']): EplDocRef | null {\n if (p.classes.length) {\n const c = p.classes[0]\n return { id: `class:${c.id >>> 0}`, kind: 'class', classId: c.id, title: c.name }\n }\n const order: Exclude<EplDocKind, 'class'>[] = [\n 'globals',\n 'structs',\n 'dll',\n 'constants',\n 'resources',\n 'forms',\n 'libraries',\n ]\n const counts: Record<string, number> = {\n globals: p.globals.length,\n structs: p.structs.length,\n dll: p.dllDeclares.length,\n constants: p.constants.length,\n resources: p.resources.length,\n forms: p.forms.length,\n libraries: p.libraries.length,\n }\n for (const k of order) {\n if (counts[k] > 0) return { id: k, kind: k, title: TABLE_TITLE[k] }\n }\n return null\n}\n\nwatch(preview, (p) => {\n openTabs.value = []\n activeId.value = ''\n sideTab.value = 'program'\n if (p) {\n const d = defaultDoc(p.program)\n if (d) openDoc(d)\n }\n})\n\nfunction openDoc(ref: EplDocRef): void {\n if (!openTabs.value.some((t) => t.id === ref.id)) openTabs.value = [...openTabs.value, ref]\n activeId.value = ref.id\n}\n\nfunction closeTab(id: string): void {\n const idx = openTabs.value.findIndex((t) => t.id === id)\n if (idx < 0) return\n const next = openTabs.value.filter((t) => t.id !== id)\n openTabs.value = next\n if (activeId.value === id) {\n const fallback = next[idx] ?? next[idx - 1] ?? null\n activeId.value = fallback ? fallback.id : ''\n }\n}\n\nasync function onSelectMethod(payload: { classId: number; methodId: number }): Promise<void> {\n const p = program.value\n if (!p) return\n const c = p.classes.find((x) => x.id === payload.classId)\n if (!c) return\n openDoc({\n id: `class:${payload.classId >>> 0}`,\n kind: 'class',\n classId: payload.classId,\n title: c.name,\n })\n await nextTick()\n document.getElementById(`epl-m-${payload.methodId >>> 0}`)?.scrollIntoView({ block: 'start' })\n}\n\nfunction openLibraries(): void {\n openDoc({ id: 'libraries', kind: 'libraries', title: '模块引用表' })\n}\n</script>\n\n<template>\n <div class=\"epl\">\n <p v-if=\"status === 'loading'\" class=\"epl__msg\">正在解析易语言工程…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"epl__msg epl__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'encrypted'\" class=\"epl__msg epl__gate\">\n <p>这是加密的易语言工程文件,暂未支持解密预览。</p>\n <p v-if=\"encryptTip\" class=\"epl__tip\">密码提示:{{ encryptTip }}</p>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"epl__msg epl__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"status === 'ready' && meta && program\" class=\"epl__ide\">\n <!-- top bar: project metadata -->\n <div class=\"epl__top\">\n <span class=\"epl__name\">{{ meta.project?.name || props.name }}</span>\n <span class=\"epl__badge\">{{ fileTypeLabel }}</span>\n <span class=\"epl__counts\">\n 程序集 {{ meta.counts.classes }} · 子程序 {{ meta.counts.methods }} · 数据类型\n {{ meta.counts.structs }} · DLL {{ meta.counts.dllDeclares }}\n <template v-if=\"meta.systemVersion\"> · 易语言 {{ meta.systemVersion }}</template>\n </span>\n </div>\n\n <div class=\"epl__body\">\n <!-- left: tree + side tabs -->\n <aside class=\"epl__side\">\n <div class=\"epl__sidebody\">\n <EplTree\n v-show=\"sideTab === 'program'\"\n :program=\"program\"\n :active-id=\"activeId\"\n @select=\"openDoc\"\n @select-method=\"onSelectMethod\"\n />\n <div v-show=\"sideTab === 'libs'\" class=\"epl__libs\">\n <div class=\"epl__libsroot\">支持库</div>\n <button\n v-for=\"(l, i) in program.libraries\"\n :key=\"i\"\n type=\"button\"\n class=\"epl__lib\"\n :title=\"l.fileName\"\n @click=\"openLibraries\"\n >\n <span class=\"epl__libico\">库</span>{{ l.name || l.fileName }}\n </button>\n <p v-if=\"!program.libraries.length\" class=\"epl__libsempty\">(无引用支持库)</p>\n </div>\n </div>\n <div class=\"epl__sidetabs\">\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'libs' }\"\n @click=\"sideTab = 'libs'\"\n >\n 支持库\n </button>\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'program' }\"\n @click=\"sideTab = 'program'\"\n >\n 程序\n </button>\n </div>\n </aside>\n\n <!-- right: document + bottom tabs -->\n <main class=\"epl__main\">\n <div class=\"epl__docarea\">\n <EplDoc v-if=\"activeDoc\" :program=\"program\" :doc=\"activeDoc\" />\n <p v-else class=\"epl__msg\">从左侧选择程序集、类或表进行查看。</p>\n </div>\n <div v-if=\"openTabs.length\" class=\"epl__doctabs\">\n <div\n v-for=\"t in openTabs\"\n :key=\"t.id\"\n class=\"epl__doctab\"\n :class=\"{ 'is-active': t.id === activeId }\"\n >\n <button type=\"button\" class=\"epl__doctab-label\" @click=\"activeId = t.id\">\n {{ t.title }}\n </button>\n <button type=\"button\" class=\"epl__doctab-close\" title=\"关闭\" @click=\"closeTab(t.id)\">\n ×\n </button>\n </div>\n </div>\n </main>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.epl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n\n /* Fixed light \"IDE\" palette so the viewer reads like 易语言 regardless of the app theme. */\n --epl-bg: #ffffff;\n --epl-fg: #1f1f1f;\n --epl-border: #c4ccc4;\n --epl-th-bg: #dce8dc;\n --epl-th-fg: #2c3a2c;\n --epl-ident: #1b50c8;\n --epl-keyword: #0a3fb0;\n --epl-string: #c01818;\n --epl-comment: #1f8a3b;\n --epl-check: #1a7a1a;\n --epl-guide: #9aa6b2;\n --epl-gutter: #97a0a8;\n --epl-gutter-bg: #f4f6f4;\n --epl-row-hover: #eef4ff;\n --epl-sel: #cfe0ff;\n --epl-side-bg: #f6f7f6;\n --epl-tabbar-bg: #dfe3df;\n --epl-tab-bg: #e9ece9;\n --epl-tab-active-bg: #ffffff;\n}\n\n/* ── status messages ─────────────────────────────────────────────────────── */\n.epl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.epl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.epl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.epl__tip {\n color: var(--faint);\n font-size: 12.5px;\n}\n.epl__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.epl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* ── IDE shell ───────────────────────────────────────────────────────────── */\n.epl__ide {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--epl-bg);\n color: var(--epl-fg);\n}\n.epl__top {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 6px 12px;\n border-bottom: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n font-size: 12.5px;\n}\n.epl__name {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__badge {\n font-size: 11px;\n color: #0a5;\n background: #e3f3e8;\n border-radius: 999px;\n padding: 1px 8px;\n}\n.epl__counts {\n color: #5a6a5a;\n}\n\n.epl__body {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* left side */\n.epl__side {\n flex: 0 0 auto;\n width: 248px;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n}\n.epl__sidebody {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.epl__sidetabs {\n flex: 0 0 auto;\n display: flex;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__sidetabs button {\n flex: 1 1 0;\n padding: 5px 8px;\n border: none;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n}\n.epl__sidetabs button:last-child {\n border-right: none;\n}\n.epl__sidetabs button.is-active {\n background: var(--epl-tab-active-bg);\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__libs {\n padding: 6px 0;\n font-size: 12.5px;\n}\n.epl__libsroot {\n padding: 2px 10px 4px;\n font-weight: 600;\n color: var(--epl-th-fg);\n}\n.epl__lib {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 2px 10px 2px 18px;\n border: none;\n background: none;\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: inherit;\n}\n.epl__lib:hover {\n color: var(--epl-ident);\n}\n.epl__libico {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n border-radius: 3px;\n font-size: 10px;\n color: #fff;\n background: #c08a4a;\n}\n.epl__libsempty {\n padding: 4px 12px;\n color: var(--epl-gutter);\n}\n\n/* right side */\n.epl__main {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.epl__docarea {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--epl-bg);\n}\n.epl__doctabs {\n flex: 0 0 auto;\n display: flex;\n overflow-x: auto;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__doctab {\n display: flex;\n align-items: center;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n white-space: nowrap;\n}\n.epl__doctab.is-active {\n background: var(--epl-tab-active-bg);\n}\n.epl__doctab-label {\n padding: 5px 6px 5px 12px;\n border: none;\n background: none;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.epl__doctab.is-active .epl__doctab-label {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__doctab-close {\n padding: 0 8px 0 2px;\n border: none;\n background: none;\n cursor: pointer;\n color: #999;\n font-size: 14px;\n line-height: 1;\n}\n.epl__doctab-close:hover {\n color: var(--epl-string);\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EplViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/EplViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { decompileEpl, EplEncryptedError, type EplPreview } from '@/lib/epl'\nimport EplTree from './epl/EplTree.vue'\nimport EplDoc from './epl/EplDoc.vue'\nimport type { EplDocRef, EplDocKind } from './epl/docTypes'\n\n/**\n * IDE-style viewer for e-language (易语言) `.e` / `.ec` project files. Fetches the BYTES and parses + decompiles\n * them entirely in-browser via {@link decompileEpl} (port of OpenEpl/EProjectFile, see lib/epl) — no dependency,\n * like the eml / mht / binobj viewers. Lays the result out like the 易语言 IDE: a「程序」tree on the left\n * (程序集 / 类 + project tables), the selected document on the right (definitions as bordered tables, method\n * bodies with dashed block guides), open documents as bottom tabs, and 支持库/程序 tabs under the tree.\n *\n * Encrypted files surface a friendly message (decryption is a separate path); a large file is gated behind a\n * confirm (all parsing is in-memory). Entry shape mirrors the other self-fetching viewers: a real download URL,\n * or a blob URL minted by an archive browser from extracted bytes.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error' | 'encrypted'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst encryptTip = ref('')\nconst preview = ref<EplPreview | null>(null)\n\nconst SIZE_LIMIT = 32 * 1024 * 1024\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n encryptTip.value = ''\n preview.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n preview.value = decompileEpl(new Uint8Array(buf))\n status.value = 'ready'\n } catch (e) {\n if (e instanceof EplEncryptedError) {\n status.value = 'encrypted'\n encryptTip.value = e.tip ?? ''\n } else {\n status.value = 'error'\n errorMsg.value = e instanceof Error ? e.message : String(e)\n }\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\nwatch(() => props.url, load, { immediate: true })\n\nconst meta = computed(() => preview.value?.meta ?? null)\nconst program = computed(() => preview.value?.program ?? null)\nconst fileTypeLabel = computed(() => {\n const t = meta.value?.fileType\n return t === 'source' ? '源码 .e' : t === 'module' ? '模块 .ec' : '未知'\n})\n\n// ── IDE state: side tab + open document tabs ──────────────────────────────────────────────────────\nconst sideTab = ref<'program' | 'libs'>('program')\nconst openTabs = ref<EplDocRef[]>([])\nconst activeId = ref('')\nconst activeDoc = computed(() => openTabs.value.find((t) => t.id === activeId.value) ?? null)\n\nconst TABLE_TITLE: Record<Exclude<EplDocKind, 'class'>, string> = {\n globals: '全局变量',\n structs: '自定义数据类型',\n dll: 'Dll命令',\n constants: '常量表',\n resources: '图片/声音',\n forms: '资源表',\n libraries: '模块引用表',\n}\n\nfunction defaultDoc(p: NonNullable<typeof preview.value>['program']): EplDocRef | null {\n if (p.classes.length) {\n const c = p.classes[0]\n return { id: `class:${c.id >>> 0}`, kind: 'class', classId: c.id, title: c.name }\n }\n const order: Exclude<EplDocKind, 'class'>[] = [\n 'globals',\n 'structs',\n 'dll',\n 'constants',\n 'resources',\n 'forms',\n 'libraries',\n ]\n const counts: Record<string, number> = {\n globals: p.globals.length,\n structs: p.structs.length,\n dll: p.dllDeclares.length,\n constants: p.constants.length,\n resources: p.resources.length,\n forms: p.forms.length,\n libraries: p.libraries.length,\n }\n for (const k of order) {\n if (counts[k] > 0) return { id: k, kind: k, title: TABLE_TITLE[k] }\n }\n return null\n}\n\nwatch(preview, (p) => {\n openTabs.value = []\n activeId.value = ''\n sideTab.value = 'program'\n if (p) {\n const d = defaultDoc(p.program)\n if (d) openDoc(d)\n }\n})\n\nfunction openDoc(ref: EplDocRef): void {\n if (!openTabs.value.some((t) => t.id === ref.id)) openTabs.value = [...openTabs.value, ref]\n activeId.value = ref.id\n}\n\nfunction closeTab(id: string): void {\n const idx = openTabs.value.findIndex((t) => t.id === id)\n if (idx < 0) return\n const next = openTabs.value.filter((t) => t.id !== id)\n openTabs.value = next\n if (activeId.value === id) {\n const fallback = next[idx] ?? next[idx - 1] ?? null\n activeId.value = fallback ? fallback.id : ''\n }\n}\n\nasync function onSelectMethod(payload: { classId: number; methodId: number }): Promise<void> {\n const p = program.value\n if (!p) return\n const c = p.classes.find((x) => x.id === payload.classId)\n if (!c) return\n openDoc({\n id: `class:${payload.classId >>> 0}`,\n kind: 'class',\n classId: payload.classId,\n title: c.name,\n })\n await nextTick()\n document.getElementById(`epl-m-${payload.methodId >>> 0}`)?.scrollIntoView({ block: 'start' })\n}\n\nfunction openLibraries(): void {\n openDoc({ id: 'libraries', kind: 'libraries', title: '模块引用表' })\n}\n</script>\n\n<template>\n <div class=\"epl\">\n <p v-if=\"status === 'loading'\" class=\"epl__msg\">正在解析易语言工程…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"epl__msg epl__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'encrypted'\" class=\"epl__msg epl__gate\">\n <p>这是加密的易语言工程文件,暂未支持解密预览。</p>\n <p v-if=\"encryptTip\" class=\"epl__tip\">密码提示:{{ encryptTip }}</p>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"epl__msg epl__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"status === 'ready' && meta && program\" class=\"epl__ide\">\n <!-- top bar: project metadata -->\n <div class=\"epl__top\">\n <span class=\"epl__name\">{{ meta.project?.name || props.name }}</span>\n <span class=\"epl__badge\">{{ fileTypeLabel }}</span>\n <span class=\"epl__counts\">\n 程序集 {{ meta.counts.classes }} · 子程序 {{ meta.counts.methods }} · 数据类型\n {{ meta.counts.structs }} · DLL {{ meta.counts.dllDeclares }}\n <template v-if=\"meta.systemVersion\"> · 易语言 {{ meta.systemVersion }}</template>\n </span>\n </div>\n\n <div class=\"epl__body\">\n <!-- left: tree + side tabs -->\n <aside class=\"epl__side\">\n <div class=\"epl__sidebody\">\n <EplTree\n v-show=\"sideTab === 'program'\"\n :program=\"program\"\n :active-id=\"activeId\"\n @select=\"openDoc\"\n @select-method=\"onSelectMethod\"\n />\n <div v-show=\"sideTab === 'libs'\" class=\"epl__libs\">\n <div class=\"epl__libsroot\">支持库</div>\n <button\n v-for=\"(l, i) in program.libraries\"\n :key=\"i\"\n type=\"button\"\n class=\"epl__lib\"\n :title=\"l.fileName\"\n @click=\"openLibraries\"\n >\n <span class=\"epl__libico\">库</span>{{ l.name || l.fileName }}\n </button>\n <p v-if=\"!program.libraries.length\" class=\"epl__libsempty\">(无引用支持库)</p>\n </div>\n </div>\n <div class=\"epl__sidetabs\">\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'libs' }\"\n @click=\"sideTab = 'libs'\"\n >\n 支持库\n </button>\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'program' }\"\n @click=\"sideTab = 'program'\"\n >\n 程序\n </button>\n </div>\n </aside>\n\n <!-- right: document + bottom tabs -->\n <main class=\"epl__main\">\n <div class=\"epl__docarea\">\n <EplDoc v-if=\"activeDoc\" :program=\"program\" :doc=\"activeDoc\" />\n <p v-else class=\"epl__msg\">从左侧选择程序集、类或表进行查看。</p>\n </div>\n <div v-if=\"openTabs.length\" class=\"epl__doctabs\">\n <div\n v-for=\"t in openTabs\"\n :key=\"t.id\"\n class=\"epl__doctab\"\n :class=\"{ 'is-active': t.id === activeId }\"\n >\n <button type=\"button\" class=\"epl__doctab-label\" @click=\"activeId = t.id\">\n {{ t.title }}\n </button>\n <button type=\"button\" class=\"epl__doctab-close\" title=\"关闭\" @click=\"closeTab(t.id)\">\n ×\n </button>\n </div>\n </div>\n </main>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.epl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n\n /* Fixed light \"IDE\" palette so the viewer reads like 易语言 regardless of the app theme. */\n --epl-bg: #ffffff;\n --epl-fg: #1f1f1f;\n --epl-border: #c4ccc4;\n --epl-th-bg: #dce8dc;\n --epl-th-fg: #2c3a2c;\n --epl-ident: #1b50c8;\n --epl-keyword: #0a3fb0;\n --epl-string: #c01818;\n --epl-comment: #1f8a3b;\n --epl-check: #1a7a1a;\n --epl-guide: #9aa6b2;\n --epl-gutter: #97a0a8;\n --epl-gutter-bg: #f4f6f4;\n --epl-row-hover: #eef4ff;\n --epl-sel: #cfe0ff;\n --epl-side-bg: #f6f7f6;\n --epl-tabbar-bg: #dfe3df;\n --epl-tab-bg: #e9ece9;\n --epl-tab-active-bg: #ffffff;\n}\n\n/* ── status messages ─────────────────────────────────────────────────────── */\n.epl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.epl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.epl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.epl__tip {\n color: var(--faint);\n font-size: 12.5px;\n}\n.epl__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.epl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* ── IDE shell ───────────────────────────────────────────────────────────── */\n.epl__ide {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--epl-bg);\n color: var(--epl-fg);\n}\n.epl__top {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 6px 12px;\n border-bottom: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n font-size: 12.5px;\n}\n.epl__name {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__badge {\n font-size: 11px;\n color: #0a5;\n background: #e3f3e8;\n border-radius: 999px;\n padding: 1px 8px;\n}\n.epl__counts {\n color: #5a6a5a;\n}\n\n.epl__body {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* left side */\n.epl__side {\n flex: 0 0 auto;\n width: 248px;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n}\n.epl__sidebody {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.epl__sidetabs {\n flex: 0 0 auto;\n display: flex;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__sidetabs button {\n flex: 1 1 0;\n padding: 5px 8px;\n border: none;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n}\n.epl__sidetabs button:last-child {\n border-right: none;\n}\n.epl__sidetabs button.is-active {\n background: var(--epl-tab-active-bg);\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__libs {\n padding: 6px 0;\n font-size: 12.5px;\n}\n.epl__libsroot {\n padding: 2px 10px 4px;\n font-weight: 600;\n color: var(--epl-th-fg);\n}\n.epl__lib {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 2px 10px 2px 18px;\n border: none;\n background: none;\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: inherit;\n}\n.epl__lib:hover {\n color: var(--epl-ident);\n}\n.epl__libico {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n border-radius: 3px;\n font-size: 10px;\n color: #fff;\n background: #c08a4a;\n}\n.epl__libsempty {\n padding: 4px 12px;\n color: var(--epl-gutter);\n}\n\n/* right side */\n.epl__main {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.epl__docarea {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--epl-bg);\n}\n.epl__doctabs {\n flex: 0 0 auto;\n display: flex;\n overflow-x: auto;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__doctab {\n display: flex;\n align-items: center;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n white-space: nowrap;\n}\n.epl__doctab.is-active {\n background: var(--epl-tab-active-bg);\n}\n.epl__doctab-label {\n padding: 5px 6px 5px 12px;\n border: none;\n background: none;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.epl__doctab.is-active .epl__doctab-label {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__doctab-close {\n padding: 0 8px 0 2px;\n border: none;\n background: none;\n cursor: pointer;\n color: #999;\n font-size: 14px;\n line-height: 1;\n}\n.epl__doctab-close:hover {\n color: var(--epl-string);\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileViewerBody.js","names":[],"sources":["../../src/components/FileViewerBody.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, ref, watch } from 'vue'\nimport { optionalViewer } from '@/components/optionalViewer'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { skipsTextLoad } from '@/lib/viewerKind'\nimport {\n VIEWER_FORMATTERS as FORMATTERS,\n applyFormatter,\n findFormatter,\n type ViewerFormatter,\n} from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport { looksLikeMindMap } from '@/lib/mindmap'\nimport { parseFrontmatter } from '@/lib/frontmatter'\nimport { formatBytes } from '@/lib/format'\nimport CodeView from '@/components/CodeView.vue'\nimport MarkdownText from '@/components/MarkdownText'\nimport FrontmatterCard from '@/components/FrontmatterCard.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's CONTENT area — the type dispatch shared by {@link FileViewerPanel} (a top-level file) and\n * the archive browsers (a file inside a zip/7z/tar/rar, extracted to a blob URL). Given a {@link ViewerFile}\n * it renders the right thing: a self-fetching sub-viewer (docx/pdf/dicom/…), an inline image/video, or — for\n * the text-backed kinds — markdown / html / svg / mind-map / subtitle / geo / csv (rendered), or highlighted,\n * foldable source. It fetches its own text for the text-backed kinds and emits it via `loaded` so the host's\n * copy button can use it. The host owns the header/bar, the render↔source `mode`, and copy/download.\n *\n * Extracted from FileViewerPanel so an archive entry previews with the EXACT same viewers as a top-level file\n * (the old archive detail view only did text/image/office/dicom). The archive browsers import this lazily to\n * break the import cycle (this component lazy-imports them right back for nested archives).\n *\n * \"Open with\" override: ANY file can be force-opened as a different formatter (hex / text / markdown / html /\n * svg / image / audio / video — {@link VIEWER_FORMATTERS}), since auto-detection by extension/mime can be\n * wrong. The choice becomes an EFFECTIVE file ({@link vf}) that re-runs the dispatch. Two entry points drive\n * the SAME override: (1) the host's header menu (FileViewerPanel) calls the exposed `pickFormat`/`resetFormat`\n * — the primary, universal entry; (2) the unpreviewable-binary card's own chip row, for the archive-browser\n * context where there is no host header. The effective file is emitted via `formatchange` (so the host tracks\n * toggle/copy visibility) and the active key via `formatpick` (so the host menu highlights it). `hostChooser`\n * hides the in-body switcher bar when the host already shows the chooser in its header (no duplicate UI).\n */\nconst props = withDefaults(\n defineProps<{\n file: ViewerFile\n mode?: 'render' | 'source'\n hostChooser?: boolean\n /**\n * Render-only HTML whose archive-relative dependencies have already been rewritten. Source/copy continue to\n * use `file.content` (or fetched text), so opening an HTML entry inside a ZIP still shows its original source.\n */\n htmlPreview?: string\n }>(),\n { mode: 'render', hostChooser: false },\n)\nconst emit = defineEmits<{\n loaded: [text: string]\n formatchange: [file: ViewerFile]\n formatpick: [key: string]\n}>()\n\n// Archives import this body; lazy-import them back so the module cycle never evaluates eagerly.\nconst ZipBrowser = defineAsyncComponent(() => import('@/components/ZipBrowser.vue'))\nconst SevenZipBrowser = defineAsyncComponent(() => import('@/components/SevenZipBrowser.vue'))\nconst ArchiveBrowser = defineAsyncComponent(() => import('@/components/ArchiveBrowser.vue'))\nconst BinObjBrowser = defineAsyncComponent(() => import('@/components/BinObjBrowser.vue'))\nconst EplViewer = defineAsyncComponent(() => import('@/components/EplViewer.vue'))\nconst ClassViewer = defineAsyncComponent(() => import('@/components/ClassViewer.vue'))\nconst HprofViewer = defineAsyncComponent(() => import('@/components/HprofViewer.vue'))\nconst MsiViewer = defineAsyncComponent(() => import('@/components/MsiViewer.vue'))\nconst AppxViewer = defineAsyncComponent(() => import('@/components/AppxViewer.vue'))\n// Heavy viewers whose engine is a STATIC optional peer dep: wrap so a missing dep shows an install card.\nconst DicomViewer = optionalViewer(\n () => import('@/components/DicomViewer.vue'),\n 'dicom-parser',\n 'DICOM 影像',\n)\nconst DocxViewer = optionalViewer(\n () => import('@/components/DocxViewer.vue'),\n 'docx-preview',\n 'Word 文档',\n)\nconst XlsxViewer = optionalViewer(\n () => import('@/components/XlsxViewer.vue'),\n ['exceljs', 'numfmt'],\n 'Excel 表格',\n)\nconst PptxViewer = optionalViewer(\n () => import('@/components/PptxViewer.vue'),\n ['@aiden0z/pptx-renderer', 'pdfjs-dist'],\n 'PowerPoint',\n)\nconst RtfViewer = defineAsyncComponent(() => import('@/components/RtfViewer.vue'))\nconst PdfViewer = optionalViewer(() => import('@/components/PdfViewer.vue'), 'pdfjs-dist', 'PDF')\nconst MindMapView = defineAsyncComponent(() => import('@/components/MindMapView.vue'))\nconst XmindViewer = defineAsyncComponent(() => import('@/components/XmindViewer.vue'))\nconst EmlViewer = defineAsyncComponent(() => import('@/components/EmlViewer.vue'))\nconst MhtViewer = defineAsyncComponent(() => import('@/components/MhtViewer.vue'))\nconst HarViewer = defineAsyncComponent(() => import('@/components/HarViewer.vue'))\nconst SqliteViewer = optionalViewer(\n () => import('@/components/SqliteViewer.vue'),\n 'sql.js',\n 'SQLite 数据库',\n)\nconst SubtitleViewer = defineAsyncComponent(() => import('@/components/SubtitleViewer.vue'))\nconst FontViewer = defineAsyncComponent(() => import('@/components/FontViewer.vue'))\nconst NotebookViewer = defineAsyncComponent(() => import('@/components/NotebookViewer.vue'))\nconst GeoViewer = defineAsyncComponent(() => import('@/components/GeoViewer.vue'))\nconst CsvViewer = defineAsyncComponent(() => import('@/components/CsvViewer.vue'))\nconst JsonlViewer = defineAsyncComponent(() => import('@/components/JsonlViewer.vue'))\nconst CertViewer = defineAsyncComponent(() => import('@/components/CertViewer.vue'))\nconst JksViewer = defineAsyncComponent(() => import('@/components/JksViewer.vue'))\nconst AudioViewer = defineAsyncComponent(() => import('@/components/AudioViewer.vue'))\nconst QmcAudioViewer = defineAsyncComponent(() => import('@/components/QmcAudioViewer.vue'))\nconst LrcViewer = defineAsyncComponent(() => import('@/components/LrcViewer.vue'))\nconst LogViewer = defineAsyncComponent(() => import('@/components/LogViewer.vue'))\nconst ModelViewer = optionalViewer(() => import('@/components/ModelViewer.vue'), 'three', '3D 模型')\nconst RasterViewer = defineAsyncComponent(() => import('@/components/RasterViewer.vue'))\nconst PsdViewer = defineAsyncComponent(() => import('@/components/PsdViewer.vue'))\nconst EbookViewer = defineAsyncComponent(() => import('@/components/EbookViewer.vue'))\nconst DrawioViewer = defineAsyncComponent(() => import('@/components/DrawioViewer.vue'))\nconst DxfViewer = defineAsyncComponent(() => import('@/components/DxfViewer.vue'))\nconst HexViewer = defineAsyncComponent(() => import('@/components/HexViewer.vue'))\n\n// ── \"open with\" override ──────────────────────────────────────────────────────────────────────\n// A user-chosen formatter ({@link VIEWER_FORMATTERS}). When set it replaces props.file as the EFFECTIVE file\n// the dispatch (and load) use; cleared whenever a different file is opened. Driven by the host header menu\n// (via the exposed pickFormat/resetFormat) and/or the in-body binary-card chips — both funnel through `pick`.\nconst pickedKey = ref('')\nconst picked = ref<ViewerFile | null>(null)\n// The effective file: the user's chosen formatter, else the file passed in.\nconst vf = computed<ViewerFile>(() => picked.value ?? props.file)\n\nfunction pick(f: ViewerFormatter): void {\n pickedKey.value = f.key\n picked.value = applyFormatter(props.file, f)\n}\n// Host-driven entry points (FileViewerPanel's header menu): pick by key, or fall back to auto-detection.\nfunction pickFormat(key: string): void {\n const f = findFormatter(key)\n if (f) pick(f)\n}\nfunction resetFormat(): void {\n pickedKey.value = ''\n picked.value = null\n}\n// Announce the active formatter so the host header can highlight it (fires on pick, reset, and file change).\nwatch(pickedKey, (k) => emit('formatpick', k))\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst text = ref('')\nconst errorMsg = ref('')\n\n// Mind map only renders the diagram once the loaded text actually looks like one (a non-mind-map `.mm` reads\n// as plain source). Same content sniff the panel used.\nconst isMindMap = computed(\n () =>\n vf.value.mindmap === true &&\n status.value === 'ready' &&\n looksLikeMindMap(text.value, fileExt(vf.value.name)),\n)\n\n// Markdown render: lift any leading YAML frontmatter into a card; the source view still shows the raw file.\nconst frontmatter = computed(() =>\n vf.value.lang === 'markdown' ? parseFrontmatter(text.value) : { data: {}, body: text.value },\n)\nconst hasFrontmatter = computed(() => Object.keys(frontmatter.value.data).length > 0)\n\n// Drop a single trailing newline so the source view doesn't show a phantom last line.\nconst sourceText = computed(() => text.value.replace(/\\n$/, ''))\n\n// SVG render: rasterize the markup via a data-URL <img> (an <img> never runs SVG scripts).\nconst svgSrc = computed(() =>\n text.value ? `data:image/svg+xml,${encodeURIComponent(text.value)}` : '',\n)\n\n// HTML preview, two modes (default = inert):\n// - INERT (default): the iframe carries an empty `sandbox` (no `allow-scripts`) plus an injected\n// `script-src 'none'` CSP, so NO upstream script runs — only layout/CSS/images render.\n// - INTERACTIVE (opt-in via the toolbar): scripts run inside a HARDENED sandbox — `allow-scripts` WITHOUT\n// `allow-same-origin` gives the frame an opaque origin (no access to our cookies/storage/session/DOM); that\n// opaque origin is the REAL isolation. The CSP allows external sub-resources AND network so CDN-based pages\n// fully work — scripts/styles/fonts/images from unpkg/jsdelivr/Google Fonts, plus fetch/XHR/WebSocket and\n// source-map loads (`connect-src` open to https/http/ws/wss). Only the structural hardening stays:\n// `object-src 'none'` (no plugins) and `base-uri 'none'` (no <base> hijack); forms are already blocked by\n// the sandbox (no `allow-forms`). Because the origin is opaque, even with network open the page can't reach\n// our session/cookies — it acts only with its own (already-visible) content. Review the source first\n// (render/source toggle), then enable. The ⛶ button opens this SAME sandboxed frame as a full-screen\n// in-app overlay (still an iframe → still opaque-origin), so a big interactive view stays safe — there's\n// no top-level / new-tab path that could ever run scripts in our origin.\nconst INERT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst INTERACTIVE_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"default-src 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; ` +\n `connect-src data: blob: https: http: ws: wss:; object-src 'none'; base-uri 'none'\">`\n\nfunction injectCsp(html: string, csp: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + csp + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${csp}</head><body>${html}</body></html>`\n}\n\nconst inertBlobUrl = ref('') // the inert (non-interactive) iframe + full-screen overlay\nconst interactiveBlobUrl = ref('') // built only when the user opts in\nconst interactive = ref(false)\nconst fullscreen = ref(false) // in-app full-screen overlay (Teleport to body) over the SAME sandboxed frame\nconst frameBlobUrl = computed(() =>\n interactive.value ? interactiveBlobUrl.value : inertBlobUrl.value,\n)\nconst previewHtml = computed(() => props.htmlPreview ?? text.value)\n\nfunction revokeHtmlBlobs(): void {\n if (inertBlobUrl.value) {\n URL.revokeObjectURL(inertBlobUrl.value)\n inertBlobUrl.value = ''\n }\n if (interactiveBlobUrl.value) {\n URL.revokeObjectURL(interactiveBlobUrl.value)\n interactiveBlobUrl.value = ''\n }\n}\n\nwatch([() => vf.value.html, previewHtml], ([html, preview]) => {\n revokeHtmlBlobs()\n interactive.value = false // a new file / changed text always returns to the safe inert default\n fullscreen.value = false // …and closes any open full-screen overlay\n if (html && typeof URL !== 'undefined' && URL.createObjectURL) {\n inertBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(preview, INERT_CSP)], { type: 'text/html' }),\n )\n }\n})\n\n// Toggle the sandboxed iframe between inert and interactive (builds the interactive blob lazily on first use).\nfunction toggleInteractive(): void {\n if (interactive.value) {\n interactive.value = false\n return\n }\n if (typeof URL !== 'undefined' && URL.createObjectURL && !interactiveBlobUrl.value) {\n interactiveBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(previewHtml.value, INTERACTIVE_CSP)], { type: 'text/html' }),\n )\n }\n interactive.value = true\n}\n\n// Full-screen overlay key handling + body scroll lock — only while open, SSR-guarded (no window in smoke).\nfunction onFsKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') fullscreen.value = false\n}\nwatch(fullscreen, (on) => {\n if (typeof window === 'undefined') return\n if (on) {\n window.addEventListener('keydown', onFsKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\nasync function load(noStore = false): Promise<void> {\n const f = vf.value\n // Self-fetching sub-viewers + image/video/binary render straight from the URL — no text to load.\n if (skipsTextLoad(f)) {\n text.value = ''\n status.value = 'ready'\n return\n }\n if (f.content != null) {\n text.value = f.content\n status.value = 'ready'\n emit('loaded', f.content)\n return\n }\n if (!f.url) {\n text.value = ''\n status.value = 'ready'\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(f.url, noStore ? { cache: 'no-store' } : {})\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const t = await res.text()\n text.value = t\n status.value = 'ready'\n emit('loaded', t)\n } catch (e) {\n errorMsg.value = String(e)\n status.value = 'error'\n }\n}\n\n// A new file resets any \"open with\" override (registered BEFORE the vf watcher so picked is cleared before the\n// effective file is re-read below).\nwatch(\n () => props.file,\n () => {\n picked.value = null\n pickedKey.value = ''\n },\n)\n// Load + announce the effective file whenever it changes (a new file, or a picked formatter).\nwatch(\n vf,\n (v) => {\n emit('formatchange', v)\n void load()\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n revokeHtmlBlobs()\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\n// The host's refresh button re-fetches (cache-busting) through here; its header menu drives the \"open with\"\n// override through pickFormat / resetFormat (see the override block above).\ndefineExpose({ reload: () => load(true), pickFormat, resetFormat })\n</script>\n\n<template>\n <div class=\"fvb\">\n <!-- forced-format switcher: shown once the user picks a formatter, so they can swap. Hidden when the host\n provides the chooser in its header (FileViewerPanel) — only the archive browsers render it in-body. -->\n <div v-if=\"picked && !hostChooser\" class=\"fvb__fmtbar\">\n <span class=\"fvb__fmtbar-label\">格式</span>\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n :class=\"{ 'fvb__fmtchip--on': pickedKey === f.key }\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n\n <!-- zip / 7z / tar-rar: the archive browsers fetch the bytes, decompress in-browser, and browse entries -->\n <ZipBrowser v-if=\"vf.zip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <SevenZipBrowser v-else-if=\"vf.sevenZip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <ArchiveBrowser v-else-if=\"vf.archive\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :kind=\"vf.archive\" />\n\n <!-- native binary object (.so/.dll): parse + list symbols -->\n <BinObjBrowser v-else-if=\"vf.binobj\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- e-language (.e/.ec): parse the binary section format + decompile each method to 易语言 source -->\n <EplViewer v-else-if=\"vf.epl\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- java bytecode (.class): parse the structural tables + list fields/methods (no decompilation) -->\n <ClassViewer v-else-if=\"vf.javaClass\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- jvm heap dump (.hprof): stream-parse → class histogram + per-class object drill-in (paginated/filterable) -->\n <HprofViewer v-else-if=\"vf.hprof\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows installer (.msi): parse the OLE compound file → product identity + summary info -->\n <MsiViewer v-else-if=\"vf.msi\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows app package (.appx/.msix): unzip the manifest → identity / deps / capabilities -->\n <AppxViewer v-else-if=\"vf.appx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- xmind workbook (.xmind): unzip content.json/xml → render as an interactive mind map -->\n <XmindViewer v-else-if=\"vf.xmind\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- dxf (.dxf): parse the ASCII CAD drawing → SVG with pan/zoom + layer toggles -->\n <DxfViewer v-else-if=\"vf.dxf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <DicomViewer v-else-if=\"vf.dicom\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <DocxViewer v-else-if=\"vf.docx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <XlsxViewer v-else-if=\"vf.xlsx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PptxViewer v-else-if=\"vf.pptx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <RtfViewer v-else-if=\"vf.rtf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PdfViewer v-else-if=\"vf.pdf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <EmlViewer v-else-if=\"vf.eml\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <MhtViewer v-else-if=\"vf.mht\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <HarViewer v-else-if=\"vf.har\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <SqliteViewer v-else-if=\"vf.sqlite\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <CertViewer v-else-if=\"vf.cert\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- java keystore (.jks/.keystore/.jceks): parse the inventory + certs; password decrypts private keys -->\n <JksViewer v-else-if=\"vf.jks\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <FontViewer v-else-if=\"vf.font\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <NotebookViewer v-else-if=\"vf.ipynb\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <AudioViewer v-else-if=\"vf.audio\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- QQ Music encrypted audio (.mflac/.mgg): decrypt in-browser → play + download decrypted -->\n <QmcAudioViewer v-else-if=\"vf.qmc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- LRC lyrics (.lrc): self-fetch + sniff GBK → synced lyric list (own render/source toggle) -->\n <LrcViewer v-else-if=\"vf.lrc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <LogViewer v-else-if=\"vf.log\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <ModelViewer v-else-if=\"vf.model\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- PSD: dedicated viewer with a live layer tree (toggle visibility → re-composite); other raster kinds\n (TIFF/HEIC/RAW) use the generic decoding viewer. -->\n <PsdViewer\n v-else-if=\"vf.raster === 'psd'\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <RasterViewer\n v-else-if=\"vf.raster\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.raster\"\n />\n <EbookViewer\n v-else-if=\"vf.ebook\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.ebook\"\n />\n\n <!-- hex dump: offset + raw bytes + ASCII (the universal \"open with\" fall-back; never auto-detected) -->\n <HexViewer v-else-if=\"vf.hex\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- image (archive entry, or forced via \"open with\"): a zoomable inline picture -->\n <div v-else-if=\"vf.image\" class=\"fvb__imagezoom\">\n <ZoomImage :src=\"vf.url ?? ''\" :alt=\"vf.name\" />\n </div>\n\n <!-- video (archive entry): native player from the blob URL -->\n <div v-else-if=\"vf.video\" class=\"fvb__video\">\n <video class=\"fvb__videoel\" :src=\"vf.url ?? ''\" controls preload=\"metadata\" />\n </div>\n\n <!-- unpreviewable binary (archive entry / unmatched file): download card + \"open with\" formatter chips -->\n <div v-else-if=\"vf.binary\" class=\"fvb__binary\">\n <Icon name=\"archive\" :size=\"34\" />\n <p>无法预览的二进制文件</p>\n <p v-if=\"vf.size\" class=\"fvb__binary-sub\">{{ formatBytes(vf.size) }}</p>\n <a class=\"fvb__dlbtn\" :href=\"vf.url\" :download=\"vf.name\">下载</a>\n <div class=\"fvb__openas\">\n <span class=\"fvb__openas-label\">用查看器打开:</span>\n <div class=\"fvb__openas-grid\">\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n </div>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"fvb__msg\">加载文件中…</p>\n <div v-else-if=\"status === 'error'\" class=\"fvb__msg fvb__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"fvb__retry\" @click=\"load(true)\">重试</button>\n </div>\n\n <!-- markdown render: frontmatter card (if any) + rendered document -->\n <div\n v-else-if=\"vf.lang === 'markdown' && mode === 'render'\"\n class=\"fvb__scroll fvb__scroll--doc\"\n >\n <div class=\"fvb__markdown\">\n <FrontmatterCard v-if=\"hasFrontmatter\" :data=\"frontmatter.data\" />\n <MarkdownText :text=\"frontmatter.body\" kp=\"fv\" />\n </div>\n </div>\n\n <!-- html render: sandboxed iframe fed by a blob. Default INERT (no scripts); the ▶ button opts into a\n HARDENED interactive sandbox (allow-scripts, opaque origin, CDN + network OK), and ⛶ opens the same\n sandboxed frame as a full-screen in-app overlay (no top-level/new-tab path). -->\n <div v-else-if=\"vf.html && mode === 'render'\" class=\"fvb__html\">\n <div class=\"fvb__htmlbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行:可加载外部 CDN 并发起网络请求(fetch/XHR/WebSocket),但无法访问本站会话 / cookie(不透明源隔离)'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,但无法访问本站会话 / cookie;建议先切到源码视图审查再启用)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"全屏预览(在应用内放大查看;可在全屏内开启交互脚本)\"\n @click=\"fullscreen = true\"\n >\n <Icon name=\"maximize\" :size=\"13\" />\n </button>\n </div>\n <!-- :key forces a full remount when toggling interactive: a `sandbox` change on an existing iframe does\n NOT re-apply to the already-loaded document, so the new doc must load on a fresh element that already\n carries `allow-scripts` (set before the element is inserted, i.e. before navigation commits). -->\n <iframe\n :key=\"interactive ? 'interactive' : 'inert'\"\n class=\"fvb__frame\"\n :src=\"frameBlobUrl\"\n title=\"HTML 预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- In-app full-screen overlay: the SAME sandboxed frame at viewport size (an iframe → opaque origin,\n never a top-level same-origin document). Esc / ✕ close it; the toolbar keeps the ▶ interactive\n toggle so scripts can be enabled while full-screen. Teleported to <body> to clear stacking contexts. -->\n <Teleport to=\"body\">\n <div v-if=\"fullscreen\" class=\"fvb__fs\">\n <div class=\"fvb__fsbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行,可联网但访问不到本站会话 / cookie'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,访问不到本站会话 / cookie)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"关闭全屏 (Esc)\"\n @click=\"fullscreen = false\"\n >\n <Icon name=\"close\" :size=\"15\" :stroke-width=\"2.2\" />\n </button>\n </div>\n <iframe\n :key=\"interactive ? 'fs-interactive' : 'fs-inert'\"\n class=\"fvb__fsframe\"\n :src=\"frameBlobUrl\"\n title=\"HTML 全屏预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </Teleport>\n </div>\n\n <!-- svg render: the markup as a picture (data-URL <img>) -->\n <div v-else-if=\"vf.svg && mode === 'render'\" class=\"fvb__svg\">\n <img class=\"fvb__svgimg\" :src=\"svgSrc\" :alt=\"vf.name\" />\n </div>\n\n <!-- mind-map render: the OPML/FreeMind XML drawn as an interactive SVG tree -->\n <div v-else-if=\"isMindMap && mode === 'render'\" class=\"fvb__fill\">\n <MindMapView :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- subtitle render: .srt/.vtt/.ass parsed into a timecoded transcript -->\n <div v-else-if=\"vf.subtitle && mode === 'render'\" class=\"fvb__fill\">\n <SubtitleViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- geo render: .geojson/.kml/.gpx drawn as an offline SVG map -->\n <div v-else-if=\"vf.geo && mode === 'render'\" class=\"fvb__fill\">\n <GeoViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- csv render: .csv/.tsv parsed into a table -->\n <div v-else-if=\"vf.csv && mode === 'render'\" class=\"fvb__fill\">\n <CsvViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- jsonl render: .jsonl/.ndjson parsed into a record list (truncated rows, click to expand) -->\n <div v-else-if=\"vf.jsonl && mode === 'render'\" class=\"fvb__fill\">\n <JsonlViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- diagram render: .drawio/.excalidraw drawn as an SVG (source toggle shows the raw XML/JSON) -->\n <div v-else-if=\"vf.diagram && mode === 'render'\" class=\"fvb__fill\">\n <DrawioViewer :text=\"text\" :name=\"vf.name\" :kind=\"vf.diagram\" />\n </div>\n\n <!-- source: everything else, or markdown/html/svg/… viewed as source — highlighted + folding + format -->\n <CodeView v-else :code=\"sourceText\" :lang=\"vf.lang ?? ''\" :name=\"vf.name\" />\n </div>\n</template>\n\n<style scoped>\n/* Fills the area below the host header/bar; each branch is itself a flex child that grows. */\n.fvb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Forced-format switcher bar (shown when the user opened an unhandled file via the \"open with\" chooser). */\n.fvb__fmtbar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.fvb__fmtbar-label {\n margin-right: 2px;\n font-size: 11.5px;\n color: var(--faint);\n}\n/* Chip used both in the switcher bar and on the unpreviewable-binary card's \"open with\" row. */\n.fvb__fmtchip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\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: 999px;\n padding: 5px 11px;\n cursor: pointer;\n transition:\n background 0.15s,\n border-color 0.15s,\n color 0.15s;\n}\n.fvb__fmtchip:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.fvb__fmtchip--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__fmtchip--on:hover {\n color: #fff;\n}\n\n.fvb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.fvb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.fvb__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.fvb__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.fvb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n.fvb__scroll--doc {\n overflow: auto;\n}\n.fvb__markdown {\n padding: 18px 24px 80px;\n}\n\n/* HTML render: the iframe fills the area; a small overlay toolbar (top-right) holds the open-in-new-tab\n * button. White canvas so documents that don't set their own background read as a normal page. */\n.fvb__html {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.fvb__htmlbar {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 2;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.fvb__htmlbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n text-decoration: none;\n cursor: pointer;\n box-shadow: var(--shadow);\n}\n.fvb__htmlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n/* Interactive scripts enabled: accent the toggle so the elevated (but sandboxed) state is obvious. */\n.fvb__htmlbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__htmlbtn--on:hover {\n color: #fff;\n filter: brightness(1.06);\n}\n.fvb__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* In-app full-screen HTML overlay (Teleport to body) — same sandboxed frame at viewport size. */\n.fvb__fs {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n flex-direction: column;\n background: rgba(0, 0, 0, 0.82);\n animation: fvb-fs-in 0.12s ease-out;\n}\n@keyframes fvb-fs-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.fvb__fsbar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n padding: 8px 10px;\n}\n.fvb__fsframe {\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n border: none;\n background: #fff;\n}\n\n/* Generic \"viewer owns the area below\" host (mind map / subtitle / geo / csv). */\n.fvb__fill {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n.fvb__svg {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--surface-2);\n}\n.fvb__svgimg {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n\n.fvb__imagezoom {\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n background: var(--code-bg);\n}\n\n.fvb__video {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: #000;\n}\n.fvb__videoel {\n max-width: 100%;\n max-height: 100%;\n}\n\n.fvb__binary {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 32px;\n color: var(--faint);\n text-align: center;\n}\n.fvb__binary p {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.fvb__binary-sub {\n font-size: 11.5px;\n color: var(--faint) !important;\n}\n.fvb__dlbtn {\n margin-top: 8px;\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: 6px 16px;\n text-decoration: none;\n cursor: pointer;\n}\n.fvb__dlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* \"Open with\" formatter chooser on the unpreviewable-binary card. */\n.fvb__openas {\n margin-top: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 9px;\n}\n.fvb__openas-label {\n font-size: 12px;\n color: var(--muted);\n}\n.fvb__openas-grid {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 7px;\n max-width: 320px;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileViewerBody.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/FileViewerBody.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, onBeforeUnmount, ref, watch } from 'vue'\nimport { optionalViewer } from '@/components/optionalViewer'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { skipsTextLoad } from '@/lib/viewerKind'\nimport {\n VIEWER_FORMATTERS as FORMATTERS,\n applyFormatter,\n findFormatter,\n type ViewerFormatter,\n} from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport { looksLikeMindMap } from '@/lib/mindmap'\nimport { parseFrontmatter } from '@/lib/frontmatter'\nimport { formatBytes } from '@/lib/format'\nimport CodeView from '@/components/CodeView.vue'\nimport MarkdownText from '@/components/MarkdownText'\nimport FrontmatterCard from '@/components/FrontmatterCard.vue'\nimport ZoomImage from '@/components/ZoomImage.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * The side viewer's CONTENT area — the type dispatch shared by {@link FileViewerPanel} (a top-level file) and\n * the archive browsers (a file inside a zip/7z/tar/rar, extracted to a blob URL). Given a {@link ViewerFile}\n * it renders the right thing: a self-fetching sub-viewer (docx/pdf/dicom/…), an inline image/video, or — for\n * the text-backed kinds — markdown / html / svg / mind-map / subtitle / geo / csv (rendered), or highlighted,\n * foldable source. It fetches its own text for the text-backed kinds and emits it via `loaded` so the host's\n * copy button can use it. The host owns the header/bar, the render↔source `mode`, and copy/download.\n *\n * Extracted from FileViewerPanel so an archive entry previews with the EXACT same viewers as a top-level file\n * (the old archive detail view only did text/image/office/dicom). The archive browsers import this lazily to\n * break the import cycle (this component lazy-imports them right back for nested archives).\n *\n * \"Open with\" override: ANY file can be force-opened as a different formatter (hex / text / markdown / html /\n * svg / image / audio / video — {@link VIEWER_FORMATTERS}), since auto-detection by extension/mime can be\n * wrong. The choice becomes an EFFECTIVE file ({@link vf}) that re-runs the dispatch. Two entry points drive\n * the SAME override: (1) the host's header menu (FileViewerPanel) calls the exposed `pickFormat`/`resetFormat`\n * — the primary, universal entry; (2) the unpreviewable-binary card's own chip row, for the archive-browser\n * context where there is no host header. The effective file is emitted via `formatchange` (so the host tracks\n * toggle/copy visibility) and the active key via `formatpick` (so the host menu highlights it). `hostChooser`\n * hides the in-body switcher bar when the host already shows the chooser in its header (no duplicate UI).\n */\nconst props = withDefaults(\n defineProps<{\n file: ViewerFile\n mode?: 'render' | 'source'\n hostChooser?: boolean\n /**\n * Render-only HTML whose archive-relative dependencies have already been rewritten. Source/copy continue to\n * use `file.content` (or fetched text), so opening an HTML entry inside a ZIP still shows its original source.\n */\n htmlPreview?: string\n }>(),\n { mode: 'render', hostChooser: false },\n)\nconst emit = defineEmits<{\n loaded: [text: string]\n formatchange: [file: ViewerFile]\n formatpick: [key: string]\n}>()\n\n// Archives import this body; lazy-import them back so the module cycle never evaluates eagerly.\nconst ZipBrowser = defineAsyncComponent(() => import('@/components/ZipBrowser.vue'))\nconst SevenZipBrowser = defineAsyncComponent(() => import('@/components/SevenZipBrowser.vue'))\nconst ArchiveBrowser = defineAsyncComponent(() => import('@/components/ArchiveBrowser.vue'))\nconst BinObjBrowser = defineAsyncComponent(() => import('@/components/BinObjBrowser.vue'))\nconst EplViewer = defineAsyncComponent(() => import('@/components/EplViewer.vue'))\nconst ClassViewer = defineAsyncComponent(() => import('@/components/ClassViewer.vue'))\nconst HprofViewer = defineAsyncComponent(() => import('@/components/HprofViewer.vue'))\nconst MsiViewer = defineAsyncComponent(() => import('@/components/MsiViewer.vue'))\nconst AppxViewer = defineAsyncComponent(() => import('@/components/AppxViewer.vue'))\n// Heavy viewers whose engine is a STATIC optional peer dep: wrap so a missing dep shows an install card.\nconst DicomViewer = optionalViewer(\n () => import('@/components/DicomViewer.vue'),\n 'dicom-parser',\n 'DICOM 影像',\n)\nconst DocxViewer = optionalViewer(\n () => import('@/components/DocxViewer.vue'),\n 'docx-preview',\n 'Word 文档',\n)\nconst XlsxViewer = optionalViewer(\n () => import('@/components/XlsxViewer.vue'),\n ['exceljs', 'numfmt'],\n 'Excel 表格',\n)\nconst PptxViewer = optionalViewer(\n () => import('@/components/PptxViewer.vue'),\n ['@aiden0z/pptx-renderer', 'pdfjs-dist'],\n 'PowerPoint',\n)\nconst RtfViewer = defineAsyncComponent(() => import('@/components/RtfViewer.vue'))\nconst PdfViewer = optionalViewer(() => import('@/components/PdfViewer.vue'), 'pdfjs-dist', 'PDF')\nconst MindMapView = defineAsyncComponent(() => import('@/components/MindMapView.vue'))\nconst XmindViewer = defineAsyncComponent(() => import('@/components/XmindViewer.vue'))\nconst EmlViewer = defineAsyncComponent(() => import('@/components/EmlViewer.vue'))\nconst MhtViewer = defineAsyncComponent(() => import('@/components/MhtViewer.vue'))\nconst HarViewer = defineAsyncComponent(() => import('@/components/HarViewer.vue'))\nconst SqliteViewer = optionalViewer(\n () => import('@/components/SqliteViewer.vue'),\n 'sql.js',\n 'SQLite 数据库',\n)\nconst SubtitleViewer = defineAsyncComponent(() => import('@/components/SubtitleViewer.vue'))\nconst FontViewer = defineAsyncComponent(() => import('@/components/FontViewer.vue'))\nconst NotebookViewer = defineAsyncComponent(() => import('@/components/NotebookViewer.vue'))\nconst GeoViewer = defineAsyncComponent(() => import('@/components/GeoViewer.vue'))\nconst CsvViewer = defineAsyncComponent(() => import('@/components/CsvViewer.vue'))\nconst JsonlViewer = defineAsyncComponent(() => import('@/components/JsonlViewer.vue'))\nconst CertViewer = defineAsyncComponent(() => import('@/components/CertViewer.vue'))\nconst JksViewer = defineAsyncComponent(() => import('@/components/JksViewer.vue'))\nconst AudioViewer = defineAsyncComponent(() => import('@/components/AudioViewer.vue'))\nconst QmcAudioViewer = defineAsyncComponent(() => import('@/components/QmcAudioViewer.vue'))\nconst LrcViewer = defineAsyncComponent(() => import('@/components/LrcViewer.vue'))\nconst LogViewer = defineAsyncComponent(() => import('@/components/LogViewer.vue'))\nconst ModelViewer = optionalViewer(() => import('@/components/ModelViewer.vue'), 'three', '3D 模型')\nconst RasterViewer = defineAsyncComponent(() => import('@/components/RasterViewer.vue'))\nconst PsdViewer = defineAsyncComponent(() => import('@/components/PsdViewer.vue'))\nconst EbookViewer = defineAsyncComponent(() => import('@/components/EbookViewer.vue'))\nconst DrawioViewer = defineAsyncComponent(() => import('@/components/DrawioViewer.vue'))\nconst DxfViewer = defineAsyncComponent(() => import('@/components/DxfViewer.vue'))\nconst HexViewer = defineAsyncComponent(() => import('@/components/HexViewer.vue'))\n\n// ── \"open with\" override ──────────────────────────────────────────────────────────────────────\n// A user-chosen formatter ({@link VIEWER_FORMATTERS}). When set it replaces props.file as the EFFECTIVE file\n// the dispatch (and load) use; cleared whenever a different file is opened. Driven by the host header menu\n// (via the exposed pickFormat/resetFormat) and/or the in-body binary-card chips — both funnel through `pick`.\nconst pickedKey = ref('')\nconst picked = ref<ViewerFile | null>(null)\n// The effective file: the user's chosen formatter, else the file passed in.\nconst vf = computed<ViewerFile>(() => picked.value ?? props.file)\n\nfunction pick(f: ViewerFormatter): void {\n pickedKey.value = f.key\n picked.value = applyFormatter(props.file, f)\n}\n// Host-driven entry points (FileViewerPanel's header menu): pick by key, or fall back to auto-detection.\nfunction pickFormat(key: string): void {\n const f = findFormatter(key)\n if (f) pick(f)\n}\nfunction resetFormat(): void {\n pickedKey.value = ''\n picked.value = null\n}\n// Announce the active formatter so the host header can highlight it (fires on pick, reset, and file change).\nwatch(pickedKey, (k) => emit('formatpick', k))\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst text = ref('')\nconst errorMsg = ref('')\n\n// Mind map only renders the diagram once the loaded text actually looks like one (a non-mind-map `.mm` reads\n// as plain source). Same content sniff the panel used.\nconst isMindMap = computed(\n () =>\n vf.value.mindmap === true &&\n status.value === 'ready' &&\n looksLikeMindMap(text.value, fileExt(vf.value.name)),\n)\n\n// Markdown render: lift any leading YAML frontmatter into a card; the source view still shows the raw file.\nconst frontmatter = computed(() =>\n vf.value.lang === 'markdown' ? parseFrontmatter(text.value) : { data: {}, body: text.value },\n)\nconst hasFrontmatter = computed(() => Object.keys(frontmatter.value.data).length > 0)\n\n// Drop a single trailing newline so the source view doesn't show a phantom last line.\nconst sourceText = computed(() => text.value.replace(/\\n$/, ''))\n\n// SVG render: rasterize the markup via a data-URL <img> (an <img> never runs SVG scripts).\nconst svgSrc = computed(() =>\n text.value ? `data:image/svg+xml,${encodeURIComponent(text.value)}` : '',\n)\n\n// HTML preview, two modes (default = inert):\n// - INERT (default): the iframe carries an empty `sandbox` (no `allow-scripts`) plus an injected\n// `script-src 'none'` CSP, so NO upstream script runs — only layout/CSS/images render.\n// - INTERACTIVE (opt-in via the toolbar): scripts run inside a HARDENED sandbox — `allow-scripts` WITHOUT\n// `allow-same-origin` gives the frame an opaque origin (no access to our cookies/storage/session/DOM); that\n// opaque origin is the REAL isolation. The CSP allows external sub-resources AND network so CDN-based pages\n// fully work — scripts/styles/fonts/images from unpkg/jsdelivr/Google Fonts, plus fetch/XHR/WebSocket and\n// source-map loads (`connect-src` open to https/http/ws/wss). Only the structural hardening stays:\n// `object-src 'none'` (no plugins) and `base-uri 'none'` (no <base> hijack); forms are already blocked by\n// the sandbox (no `allow-forms`). Because the origin is opaque, even with network open the page can't reach\n// our session/cookies — it acts only with its own (already-visible) content. Review the source first\n// (render/source toggle), then enable. The ⛶ button opens this SAME sandboxed frame as a full-screen\n// in-app overlay (still an iframe → still opaque-origin), so a big interactive view stays safe — there's\n// no top-level / new-tab path that could ever run scripts in our origin.\nconst INERT_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'\">`\nconst INTERACTIVE_CSP =\n `<meta http-equiv=\"Content-Security-Policy\" ` +\n `content=\"default-src 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; ` +\n `connect-src data: blob: https: http: ws: wss:; object-src 'none'; base-uri 'none'\">`\n\nfunction injectCsp(html: string, csp: string): string {\n const head = html.match(/<head[^>]*>/i)\n if (head) {\n const at = (head.index ?? 0) + head[0].length\n return html.slice(0, at) + csp + html.slice(at)\n }\n return `<!doctype html><html><head><meta charset=\"utf-8\">${csp}</head><body>${html}</body></html>`\n}\n\nconst inertBlobUrl = ref('') // the inert (non-interactive) iframe + full-screen overlay\nconst interactiveBlobUrl = ref('') // built only when the user opts in\nconst interactive = ref(false)\nconst fullscreen = ref(false) // in-app full-screen overlay (Teleport to body) over the SAME sandboxed frame\nconst frameBlobUrl = computed(() =>\n interactive.value ? interactiveBlobUrl.value : inertBlobUrl.value,\n)\nconst previewHtml = computed(() => props.htmlPreview ?? text.value)\n\nfunction revokeHtmlBlobs(): void {\n if (inertBlobUrl.value) {\n URL.revokeObjectURL(inertBlobUrl.value)\n inertBlobUrl.value = ''\n }\n if (interactiveBlobUrl.value) {\n URL.revokeObjectURL(interactiveBlobUrl.value)\n interactiveBlobUrl.value = ''\n }\n}\n\nwatch([() => vf.value.html, previewHtml], ([html, preview]) => {\n revokeHtmlBlobs()\n interactive.value = false // a new file / changed text always returns to the safe inert default\n fullscreen.value = false // …and closes any open full-screen overlay\n if (html && typeof URL !== 'undefined' && URL.createObjectURL) {\n inertBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(preview, INERT_CSP)], { type: 'text/html' }),\n )\n }\n})\n\n// Toggle the sandboxed iframe between inert and interactive (builds the interactive blob lazily on first use).\nfunction toggleInteractive(): void {\n if (interactive.value) {\n interactive.value = false\n return\n }\n if (typeof URL !== 'undefined' && URL.createObjectURL && !interactiveBlobUrl.value) {\n interactiveBlobUrl.value = URL.createObjectURL(\n new Blob([injectCsp(previewHtml.value, INTERACTIVE_CSP)], { type: 'text/html' }),\n )\n }\n interactive.value = true\n}\n\n// Full-screen overlay key handling + body scroll lock — only while open, SSR-guarded (no window in smoke).\nfunction onFsKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') fullscreen.value = false\n}\nwatch(fullscreen, (on) => {\n if (typeof window === 'undefined') return\n if (on) {\n window.addEventListener('keydown', onFsKey)\n document.body.style.overflow = 'hidden'\n } else {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\nasync function load(noStore = false): Promise<void> {\n const f = vf.value\n // Self-fetching sub-viewers + image/video/binary render straight from the URL — no text to load.\n if (skipsTextLoad(f)) {\n text.value = ''\n status.value = 'ready'\n return\n }\n if (f.content != null) {\n text.value = f.content\n status.value = 'ready'\n emit('loaded', f.content)\n return\n }\n if (!f.url) {\n text.value = ''\n status.value = 'ready'\n return\n }\n status.value = 'loading'\n errorMsg.value = ''\n try {\n const res = await fetch(f.url, noStore ? { cache: 'no-store' } : {})\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const t = await res.text()\n text.value = t\n status.value = 'ready'\n emit('loaded', t)\n } catch (e) {\n errorMsg.value = String(e)\n status.value = 'error'\n }\n}\n\n// A new file resets any \"open with\" override (registered BEFORE the vf watcher so picked is cleared before the\n// effective file is re-read below).\nwatch(\n () => props.file,\n () => {\n picked.value = null\n pickedKey.value = ''\n },\n)\n// Load + announce the effective file whenever it changes (a new file, or a picked formatter).\nwatch(\n vf,\n (v) => {\n emit('formatchange', v)\n void load()\n },\n { immediate: true },\n)\n\nonBeforeUnmount(() => {\n revokeHtmlBlobs()\n if (typeof window !== 'undefined') {\n window.removeEventListener('keydown', onFsKey)\n document.body.style.overflow = ''\n }\n})\n\n// The host's refresh button re-fetches (cache-busting) through here; its header menu drives the \"open with\"\n// override through pickFormat / resetFormat (see the override block above).\ndefineExpose({ reload: () => load(true), pickFormat, resetFormat })\n</script>\n\n<template>\n <div class=\"fvb\">\n <!-- forced-format switcher: shown once the user picks a formatter, so they can swap. Hidden when the host\n provides the chooser in its header (FileViewerPanel) — only the archive browsers render it in-body. -->\n <div v-if=\"picked && !hostChooser\" class=\"fvb__fmtbar\">\n <span class=\"fvb__fmtbar-label\">格式</span>\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n :class=\"{ 'fvb__fmtchip--on': pickedKey === f.key }\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n\n <!-- zip / 7z / tar-rar: the archive browsers fetch the bytes, decompress in-browser, and browse entries -->\n <ZipBrowser v-if=\"vf.zip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <SevenZipBrowser v-else-if=\"vf.sevenZip\" :url=\"vf.url ?? ''\" :name=\"vf.name\" />\n <ArchiveBrowser v-else-if=\"vf.archive\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :kind=\"vf.archive\" />\n\n <!-- native binary object (.so/.dll): parse + list symbols -->\n <BinObjBrowser v-else-if=\"vf.binobj\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- e-language (.e/.ec): parse the binary section format + decompile each method to 易语言 source -->\n <EplViewer v-else-if=\"vf.epl\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- java bytecode (.class): parse the structural tables + list fields/methods (no decompilation) -->\n <ClassViewer v-else-if=\"vf.javaClass\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- jvm heap dump (.hprof): stream-parse → class histogram + per-class object drill-in (paginated/filterable) -->\n <HprofViewer v-else-if=\"vf.hprof\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows installer (.msi): parse the OLE compound file → product identity + summary info -->\n <MsiViewer v-else-if=\"vf.msi\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- windows app package (.appx/.msix): unzip the manifest → identity / deps / capabilities -->\n <AppxViewer v-else-if=\"vf.appx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- xmind workbook (.xmind): unzip content.json/xml → render as an interactive mind map -->\n <XmindViewer v-else-if=\"vf.xmind\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- dxf (.dxf): parse the ASCII CAD drawing → SVG with pan/zoom + layer toggles -->\n <DxfViewer v-else-if=\"vf.dxf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <DicomViewer v-else-if=\"vf.dicom\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <DocxViewer v-else-if=\"vf.docx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <XlsxViewer v-else-if=\"vf.xlsx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PptxViewer v-else-if=\"vf.pptx\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <RtfViewer v-else-if=\"vf.rtf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <PdfViewer v-else-if=\"vf.pdf\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <EmlViewer v-else-if=\"vf.eml\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <MhtViewer v-else-if=\"vf.mht\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <HarViewer v-else-if=\"vf.har\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <SqliteViewer v-else-if=\"vf.sqlite\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <CertViewer v-else-if=\"vf.cert\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- java keystore (.jks/.keystore/.jceks): parse the inventory + certs; password decrypts private keys -->\n <JksViewer v-else-if=\"vf.jks\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <FontViewer v-else-if=\"vf.font\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <NotebookViewer v-else-if=\"vf.ipynb\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <AudioViewer v-else-if=\"vf.audio\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- QQ Music encrypted audio (.mflac/.mgg): decrypt in-browser → play + download decrypted -->\n <QmcAudioViewer v-else-if=\"vf.qmc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- LRC lyrics (.lrc): self-fetch + sniff GBK → synced lyric list (own render/source toggle) -->\n <LrcViewer v-else-if=\"vf.lrc\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <LogViewer v-else-if=\"vf.log\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <ModelViewer v-else-if=\"vf.model\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n <!-- PSD: dedicated viewer with a live layer tree (toggle visibility → re-composite); other raster kinds\n (TIFF/HEIC/RAW) use the generic decoding viewer. -->\n <PsdViewer\n v-else-if=\"vf.raster === 'psd'\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n />\n <RasterViewer\n v-else-if=\"vf.raster\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.raster\"\n />\n <EbookViewer\n v-else-if=\"vf.ebook\"\n :url=\"vf.url ?? ''\"\n :name=\"vf.name\"\n :size=\"vf.size\"\n :kind=\"vf.ebook\"\n />\n\n <!-- hex dump: offset + raw bytes + ASCII (the universal \"open with\" fall-back; never auto-detected) -->\n <HexViewer v-else-if=\"vf.hex\" :url=\"vf.url ?? ''\" :name=\"vf.name\" :size=\"vf.size\" />\n\n <!-- image (archive entry, or forced via \"open with\"): a zoomable inline picture -->\n <div v-else-if=\"vf.image\" class=\"fvb__imagezoom\">\n <ZoomImage :src=\"vf.url ?? ''\" :alt=\"vf.name\" />\n </div>\n\n <!-- video (archive entry): native player from the blob URL -->\n <div v-else-if=\"vf.video\" class=\"fvb__video\">\n <video class=\"fvb__videoel\" :src=\"vf.url ?? ''\" controls preload=\"metadata\" />\n </div>\n\n <!-- unpreviewable binary (archive entry / unmatched file): download card + \"open with\" formatter chips -->\n <div v-else-if=\"vf.binary\" class=\"fvb__binary\">\n <Icon name=\"archive\" :size=\"34\" />\n <p>无法预览的二进制文件</p>\n <p v-if=\"vf.size\" class=\"fvb__binary-sub\">{{ formatBytes(vf.size) }}</p>\n <a class=\"fvb__dlbtn\" :href=\"vf.url\" :download=\"vf.name\">下载</a>\n <div class=\"fvb__openas\">\n <span class=\"fvb__openas-label\">用查看器打开:</span>\n <div class=\"fvb__openas-grid\">\n <button\n v-for=\"f in FORMATTERS\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fvb__fmtchip\"\n @click=\"pick(f)\"\n >\n <Icon :name=\"f.icon\" :size=\"13\" /><span>{{ f.label }}</span>\n </button>\n </div>\n </div>\n </div>\n\n <p v-else-if=\"status === 'loading'\" class=\"fvb__msg\">加载文件中…</p>\n <div v-else-if=\"status === 'error'\" class=\"fvb__msg fvb__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"fvb__retry\" @click=\"load(true)\">重试</button>\n </div>\n\n <!-- markdown render: frontmatter card (if any) + rendered document -->\n <div\n v-else-if=\"vf.lang === 'markdown' && mode === 'render'\"\n class=\"fvb__scroll fvb__scroll--doc\"\n >\n <div class=\"fvb__markdown\">\n <FrontmatterCard v-if=\"hasFrontmatter\" :data=\"frontmatter.data\" />\n <MarkdownText :text=\"frontmatter.body\" kp=\"fv\" />\n </div>\n </div>\n\n <!-- html render: sandboxed iframe fed by a blob. Default INERT (no scripts); the ▶ button opts into a\n HARDENED interactive sandbox (allow-scripts, opaque origin, CDN + network OK), and ⛶ opens the same\n sandboxed frame as a full-screen in-app overlay (no top-level/new-tab path). -->\n <div v-else-if=\"vf.html && mode === 'render'\" class=\"fvb__html\">\n <div class=\"fvb__htmlbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行:可加载外部 CDN 并发起网络请求(fetch/XHR/WebSocket),但无法访问本站会话 / cookie(不透明源隔离)'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,但无法访问本站会话 / cookie;建议先切到源码视图审查再启用)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"13\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"全屏预览(在应用内放大查看;可在全屏内开启交互脚本)\"\n @click=\"fullscreen = true\"\n >\n <Icon name=\"maximize\" :size=\"13\" />\n </button>\n </div>\n <!-- :key forces a full remount when toggling interactive: a `sandbox` change on an existing iframe does\n NOT re-apply to the already-loaded document, so the new doc must load on a fresh element that already\n carries `allow-scripts` (set before the element is inserted, i.e. before navigation commits). -->\n <iframe\n :key=\"interactive ? 'interactive' : 'inert'\"\n class=\"fvb__frame\"\n :src=\"frameBlobUrl\"\n title=\"HTML 预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n loading=\"lazy\"\n referrerpolicy=\"no-referrer\"\n />\n\n <!-- In-app full-screen overlay: the SAME sandboxed frame at viewport size (an iframe → opaque origin,\n never a top-level same-origin document). Esc / ✕ close it; the toolbar keeps the ▶ interactive\n toggle so scripts can be enabled while full-screen. Teleported to <body> to clear stacking contexts. -->\n <Teleport to=\"body\">\n <div v-if=\"fullscreen\" class=\"fvb__fs\">\n <div class=\"fvb__fsbar\">\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n :class=\"{ 'fvb__htmlbtn--on': interactive }\"\n :title=\"\n interactive\n ? '交互脚本已启用(点击禁用)—— 隔离沙箱内运行,可联网但访问不到本站会话 / cookie'\n : '启用交互脚本(隔离沙箱执行:可加载 CDN、可联网,访问不到本站会话 / cookie)'\n \"\n @click=\"toggleInteractive\"\n >\n <Icon name=\"play\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fvb__htmlbtn\"\n title=\"关闭全屏 (Esc)\"\n @click=\"fullscreen = false\"\n >\n <Icon name=\"close\" :size=\"15\" :stroke-width=\"2.2\" />\n </button>\n </div>\n <iframe\n :key=\"interactive ? 'fs-interactive' : 'fs-inert'\"\n class=\"fvb__fsframe\"\n :src=\"frameBlobUrl\"\n title=\"HTML 全屏预览\"\n :sandbox=\"interactive ? 'allow-scripts' : ''\"\n referrerpolicy=\"no-referrer\"\n />\n </div>\n </Teleport>\n </div>\n\n <!-- svg render: the markup as a picture (data-URL <img>) -->\n <div v-else-if=\"vf.svg && mode === 'render'\" class=\"fvb__svg\">\n <img class=\"fvb__svgimg\" :src=\"svgSrc\" :alt=\"vf.name\" />\n </div>\n\n <!-- mind-map render: the OPML/FreeMind XML drawn as an interactive SVG tree -->\n <div v-else-if=\"isMindMap && mode === 'render'\" class=\"fvb__fill\">\n <MindMapView :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- subtitle render: .srt/.vtt/.ass parsed into a timecoded transcript -->\n <div v-else-if=\"vf.subtitle && mode === 'render'\" class=\"fvb__fill\">\n <SubtitleViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- geo render: .geojson/.kml/.gpx drawn as an offline SVG map -->\n <div v-else-if=\"vf.geo && mode === 'render'\" class=\"fvb__fill\">\n <GeoViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- csv render: .csv/.tsv parsed into a table -->\n <div v-else-if=\"vf.csv && mode === 'render'\" class=\"fvb__fill\">\n <CsvViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- jsonl render: .jsonl/.ndjson parsed into a record list (truncated rows, click to expand) -->\n <div v-else-if=\"vf.jsonl && mode === 'render'\" class=\"fvb__fill\">\n <JsonlViewer :text=\"text\" :name=\"vf.name\" />\n </div>\n\n <!-- diagram render: .drawio/.excalidraw drawn as an SVG (source toggle shows the raw XML/JSON) -->\n <div v-else-if=\"vf.diagram && mode === 'render'\" class=\"fvb__fill\">\n <DrawioViewer :text=\"text\" :name=\"vf.name\" :kind=\"vf.diagram\" />\n </div>\n\n <!-- source: everything else, or markdown/html/svg/… viewed as source — highlighted + folding + format -->\n <CodeView v-else :code=\"sourceText\" :lang=\"vf.lang ?? ''\" :name=\"vf.name\" />\n </div>\n</template>\n\n<style scoped>\n/* Fills the area below the host header/bar; each branch is itself a flex child that grows. */\n.fvb {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n\n/* Forced-format switcher bar (shown when the user opened an unhandled file via the \"open with\" chooser). */\n.fvb__fmtbar {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n flex-shrink: 0;\n padding: 7px 12px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.fvb__fmtbar-label {\n margin-right: 2px;\n font-size: 11.5px;\n color: var(--faint);\n}\n/* Chip used both in the switcher bar and on the unpreviewable-binary card's \"open with\" row. */\n.fvb__fmtchip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\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: 999px;\n padding: 5px 11px;\n cursor: pointer;\n transition:\n background 0.15s,\n border-color 0.15s,\n color 0.15s;\n}\n.fvb__fmtchip:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n.fvb__fmtchip--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__fmtchip--on:hover {\n color: #fff;\n}\n\n.fvb__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.fvb__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n}\n.fvb__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.fvb__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.fvb__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overscroll-behavior: contain;\n background: var(--code-bg);\n}\n.fvb__scroll--doc {\n overflow: auto;\n}\n.fvb__markdown {\n padding: 18px 24px 80px;\n}\n\n/* HTML render: the iframe fills the area; a small overlay toolbar (top-right) holds the open-in-new-tab\n * button. White canvas so documents that don't set their own background read as a normal page. */\n.fvb__html {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n background: #fff;\n}\n.fvb__htmlbar {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 2;\n display: flex;\n align-items: center;\n gap: 4px;\n}\n.fvb__htmlbtn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n color: var(--muted);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 7px;\n text-decoration: none;\n cursor: pointer;\n box-shadow: var(--shadow);\n}\n.fvb__htmlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n/* Interactive scripts enabled: accent the toggle so the elevated (but sandboxed) state is obvious. */\n.fvb__htmlbtn--on {\n color: #fff;\n background: var(--accent);\n border-color: var(--accent);\n}\n.fvb__htmlbtn--on:hover {\n color: #fff;\n filter: brightness(1.06);\n}\n.fvb__frame {\n display: block;\n width: 100%;\n height: 100%;\n border: none;\n background: #fff;\n}\n\n/* In-app full-screen HTML overlay (Teleport to body) — same sandboxed frame at viewport size. */\n.fvb__fs {\n position: fixed;\n inset: 0;\n z-index: 2000;\n display: flex;\n flex-direction: column;\n background: rgba(0, 0, 0, 0.82);\n animation: fvb-fs-in 0.12s ease-out;\n}\n@keyframes fvb-fs-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n.fvb__fsbar {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n padding: 8px 10px;\n}\n.fvb__fsframe {\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n border: none;\n background: #fff;\n}\n\n/* Generic \"viewer owns the area below\" host (mind map / subtitle / geo / csv). */\n.fvb__fill {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n.fvb__svg {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n background: var(--surface-2);\n}\n.fvb__svgimg {\n max-width: 100%;\n height: auto;\n object-fit: contain;\n}\n\n.fvb__imagezoom {\n flex: 1 1 auto;\n min-height: 0;\n position: relative;\n background: var(--code-bg);\n}\n\n.fvb__video {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: #000;\n}\n.fvb__videoel {\n max-width: 100%;\n max-height: 100%;\n}\n\n.fvb__binary {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 32px;\n color: var(--faint);\n text-align: center;\n}\n.fvb__binary p {\n margin: 0;\n font-size: 13px;\n color: var(--muted);\n}\n.fvb__binary-sub {\n font-size: 11.5px;\n color: var(--faint) !important;\n}\n.fvb__dlbtn {\n margin-top: 8px;\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: 6px 16px;\n text-decoration: none;\n cursor: pointer;\n}\n.fvb__dlbtn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* \"Open with\" formatter chooser on the unpreviewable-binary card. */\n.fvb__openas {\n margin-top: 18px;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 9px;\n}\n.fvb__openas-label {\n font-size: 12px;\n color: var(--muted);\n}\n.fvb__openas-grid {\n display: flex;\n flex-wrap: wrap;\n justify-content: center;\n gap: 7px;\n max-width: 320px;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileViewerPanel.js","names":[],"sources":["../../src/components/FileViewerPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useFileViewer, type ViewerFile } from '@/composables/useFileViewer'\nimport { canToggleView, skipsTextLoad } from '@/lib/viewerKind'\nimport { VIEWER_FORMATTERS, findFormatter } from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport FileViewerBody from '@/components/FileViewerBody.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Right-docked viewer for files (DetailView reflows the conversation left to make room — a split pane, not an\n * overlay). The header owns the title, the render/source toggle, and the copy/download/refresh/close actions;\n * the CONTENT is delegated to {@link FileViewerBody}, the shared type dispatcher (also used by the archive\n * browsers, so a file inside a zip previews with the same viewers). The left edge is a drag splitter.\n */\nconst { current, width, setWidth, close } = useFileViewer()\n\ntype ViewMode = 'render' | 'source'\nconst mode = ref<ViewMode>('render')\nconst text = ref('') // body emits the loaded text up so the copy button can use it\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nconst bodyRef = ref<{\n reload: () => void\n pickFormat: (key: string) => void\n resetFormat: () => void\n} | null>(null)\n\n// The EFFECTIVE file the body is actually showing — it differs from `current` when the user picks a formatter\n// for an otherwise-unpreviewable file (\"open with\", see FileViewerBody). The body announces it via\n// `formatchange`, so the header's render/source toggle + copy/refresh track the chosen format, not the raw file.\nconst shown = ref<ViewerFile | null>(null)\nconst effective = computed(() => shown.value ?? current.value)\n\n// Inline content (e.g. a loaded skill): rendered directly — no download/refresh, but copy still applies.\nconst isInline = computed(() => current.value?.content != null)\n// A file whose content comes from its own sub-viewer / URL (not the panel-loaded text): no copy/refresh.\nconst selfRendered = computed(() => skipsTextLoad(effective.value))\n// Markdown / html / svg / subtitle / geo / csv / mind-map can flip between rendered and source.\nconst canToggle = computed(() => canToggleView(effective.value, text.value))\n\n// ── \"open with\" menu ──────────────────────────────────────────────────────────────────────────\n// The universal entry point: any file can be force-opened as a different formatter (hex / text / image /\n// audio / …) when auto-detection guessed wrong. The body owns the actual override; this header menu just\n// drives it through the exposed pickFormat / resetFormat. `activeFormat` mirrors the body's current choice\n// (it announces it via `formatpick`), so the menu checks the active row and the button labels it.\nconst formatters = VIEWER_FORMATTERS\nconst menuOpen = ref(false)\nconst menuRef = ref<HTMLElement | null>(null)\nconst activeFormat = ref('')\nconst activeFormatter = computed(() => findFormatter(activeFormat.value))\n// Formatters reinterpret fetched bytes, so they need a URL — hide the menu for inline-only content (no url).\nconst showChooser = computed(() => !!current.value?.url)\nconst chooserTitle = computed(() =>\n activeFormatter.value\n ? `已强制以「${activeFormatter.value.label}」打开 —— 点击切换查看方式`\n : '打开方式 —— 选择查看器(十六进制 / 文本 / 图片 / 音频 …)',\n)\n\nfunction chooseFormat(key: string): void {\n bodyRef.value?.pickFormat(key)\n menuOpen.value = false\n}\nfunction chooseAuto(): void {\n bodyRef.value?.resetFormat()\n menuOpen.value = false\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 menuRef, so a click on either is \"inside\" and never self-closes.\nfunction onDocPointer(e: MouseEvent): void {\n if (menuRef.value && !menuRef.value.contains(e.target as Node)) menuOpen.value = false\n}\nfunction onMenuKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') menuOpen.value = false\n}\nwatch(menuOpen, (on) => {\n if (typeof document === 'undefined') return\n if (on) {\n document.addEventListener('click', onDocPointer)\n document.addEventListener('keydown', onMenuKey)\n } else {\n document.removeEventListener('click', onDocPointer)\n document.removeEventListener('keydown', onMenuKey)\n }\n})\n\n// Header: \"name · EXT\" with the extension split off and emphasized.\nconst ext = computed(() => fileExt(current.value?.name ?? '').toUpperCase())\nconst baseName = computed(() => {\n const n = current.value?.name ?? ''\n const e = fileExt(n)\n return e ? n.slice(0, n.length - e.length - 1) : n\n})\n\n// Re-load whenever a different file is opened (the panel stays mounted across files);\n// each newly opened file starts in its rendered (default) view. The body owns the actual fetch.\nwatch(current, (f) => {\n if (!f) return\n mode.value = 'render'\n text.value = ''\n shown.value = null // body re-announces via `formatchange`; clear so a stale override never lingers\n activeFormat.value = '' // a new file starts auto-detected (the body also re-announces via `formatpick`)\n menuOpen.value = false\n})\n\nasync function copy(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(text.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nconst resizing = ref(false)\n\n// Drag the left edge to resize. The pane is docked right, so moving the cursor left widens it.\n// `resizing` drives a scoped class that colors only the grip — no global body class, so the color\n// cannot leak onto the page. The page-wide resize cursor + selection lock are set inline on <body>.\nfunction onResizeStart(e: PointerEvent): void {\n e.preventDefault()\n const el = e.currentTarget as HTMLElement\n const startX = e.clientX\n const startW = width.value\n const body = document.body\n const prevCursor = body.style.cursor\n const prevSelect = body.style.userSelect\n el.setPointerCapture(e.pointerId)\n resizing.value = true\n body.style.cursor = 'col-resize'\n body.style.userSelect = 'none'\n const move = (ev: PointerEvent) => setWidth(startW + (startX - ev.clientX))\n const noSelect = (ev: Event) => ev.preventDefault()\n const up = (ev: PointerEvent) => {\n el.releasePointerCapture(ev.pointerId)\n el.removeEventListener('pointermove', move)\n el.removeEventListener('pointerup', up)\n document.removeEventListener('selectstart', noSelect)\n body.style.cursor = prevCursor\n body.style.userSelect = prevSelect\n resizing.value = false\n }\n el.addEventListener('pointermove', move)\n el.addEventListener('pointerup', up)\n document.addEventListener('selectstart', noSelect)\n}\n\nonBeforeUnmount(() => {\n clearTimeout(copyTimer)\n if (typeof document !== 'undefined') {\n document.removeEventListener('click', onDocPointer)\n document.removeEventListener('keydown', onMenuKey)\n }\n})\n</script>\n\n<template>\n <aside v-if=\"current\" class=\"fv\">\n <div\n class=\"fv__resizer\"\n :class=\"{ 'fv__resizer--active': resizing }\"\n title=\"拖动调整宽度\"\n @pointerdown=\"onResizeStart\"\n >\n <span class=\"fv__grip\" />\n </div>\n\n <header class=\"fv__head\">\n <!-- markdown / html / svg / …: flip between the rendered view and its source. -->\n <div v-if=\"canToggle\" class=\"fv__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"fv__view-btn\"\n :class=\"{ 'fv__view-btn--on': mode === 'render' }\"\n :aria-pressed=\"mode === 'render'\"\n title=\"渲染\"\n @click=\"mode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fv__view-btn\"\n :class=\"{ 'fv__view-btn--on': mode === 'source' }\"\n :aria-pressed=\"mode === 'source'\"\n title=\"源码\"\n @click=\"mode = 'source'\"\n >\n <Icon name=\"code\" :size=\"15\" />\n </button>\n </div>\n <span class=\"fv__title\" :title=\"current.name\">\n <span class=\"fv__fname\">{{ baseName }}</span>\n <template v-if=\"ext\"\n ><span class=\"fv__dot\">·</span><span class=\"fv__ext\">{{ ext }}</span></template\n >\n </span>\n <div class=\"fv__actions\">\n <!-- \"open with\" menu: universal viewer chooser — any file can be force-opened as hex/text/image/audio/…\n when auto-detection guessed wrong. Drives the body's override via the exposed pickFormat/resetFormat. -->\n <div v-if=\"showChooser\" ref=\"menuRef\" class=\"fv__menu\">\n <button\n type=\"button\"\n class=\"fv__btn fv__btn--text\"\n :class=\"{ 'fv__btn--on': activeFormat }\"\n :title=\"chooserTitle\"\n :aria-haspopup=\"true\"\n :aria-expanded=\"menuOpen\"\n @click=\"menuOpen = !menuOpen\"\n >\n <Icon name=\"apps\" :size=\"14\" /><span>{{\n activeFormatter ? activeFormatter.label : '打开方式'\n }}</span>\n </button>\n <div v-if=\"menuOpen\" class=\"fv__menu-pop\" role=\"menu\">\n <button\n type=\"button\"\n class=\"fv__menu-item\"\n :class=\"{ 'fv__menu-item--on': !activeFormat }\"\n role=\"menuitemradio\"\n :aria-checked=\"!activeFormat\"\n @click=\"chooseAuto\"\n >\n <Icon name=\"eye\" :size=\"14\" /><span class=\"fv__menu-label\">自动检测</span>\n <Icon v-if=\"!activeFormat\" name=\"check\" :size=\"15\" class=\"fv__menu-check\" />\n </button>\n <div class=\"fv__menu-sep\" />\n <button\n v-for=\"f in formatters\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fv__menu-item\"\n :class=\"{ 'fv__menu-item--on': activeFormat === f.key }\"\n role=\"menuitemradio\"\n :aria-checked=\"activeFormat === f.key\"\n @click=\"chooseFormat(f.key)\"\n >\n <Icon :name=\"f.icon\" :size=\"14\" /><span class=\"fv__menu-label\">{{ f.label }}</span>\n <Icon v-if=\"activeFormat === f.key\" name=\"check\" :size=\"15\" class=\"fv__menu-check\" />\n </button>\n </div>\n </div>\n <button v-if=\"!selfRendered\" type=\"button\" class=\"fv__btn fv__btn--text\" @click=\"copy\">\n <Icon name=\"copy\" :size=\"14\" /><span>{{ copied ? 'Copied' : 'Copy' }}</span>\n </button>\n <a\n v-if=\"!isInline\"\n class=\"fv__btn\"\n :href=\"current.url\"\n :download=\"current.name\"\n title=\"下载\"\n ><Icon name=\"download\" :size=\"15\"\n /></a>\n <button\n v-if=\"!isInline && !selfRendered\"\n type=\"button\"\n class=\"fv__btn\"\n title=\"刷新\"\n @click=\"bodyRef?.reload()\"\n >\n <Icon name=\"retry\" :size=\"15\" />\n </button>\n <button type=\"button\" class=\"fv__btn\" title=\"关闭\" @click=\"close\">\n <Icon name=\"close\" :size=\"16\" />\n </button>\n </div>\n </header>\n\n <FileViewerBody\n ref=\"bodyRef\"\n :file=\"current\"\n :mode=\"mode\"\n host-chooser\n @loaded=\"text = $event\"\n @formatchange=\"shown = $event\"\n @formatpick=\"activeFormat = $event\"\n />\n </aside>\n</template>\n\n<style scoped>\n/* In-flow flex sibling of the conversation (DetailView is the flex row), so each pane owns its own\n * scrollbar — the conversation's sits at its right edge, not hidden behind a floating overlay. */\n.fv {\n --fv-code-fs: 12.5px;\n --fv-code-lh: 21px;\n position: relative;\n flex: 0 0 var(--fv-width);\n min-width: 0;\n height: 100%;\n z-index: 55;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n border-left: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n/* Left-edge drag handle: a wide invisible hit area with a full-height rail + a small centered grip. */\n.fv__resizer {\n position: absolute;\n top: 0;\n bottom: 0;\n left: -4px;\n width: 11px;\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: col-resize;\n touch-action: none;\n}\n.fv__resizer::before {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n width: 2px;\n transform: translateX(-50%);\n background: transparent;\n transition: background 0.15s;\n}\n.fv__grip {\n position: relative;\n width: 5px;\n height: 34px;\n border-radius: 3px;\n background: var(--border-2);\n transition:\n background 0.15s,\n height 0.15s;\n}\n.fv__resizer:hover::before,\n.fv__resizer--active::before {\n background: #6ea8fe;\n}\n.fv__resizer:hover .fv__grip,\n.fv__resizer--active .fv__grip {\n background: #6ea8fe;\n}\n.fv__resizer--active .fv__grip {\n height: 48px;\n}\n\n.fv__head {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-shrink: 0;\n padding: 10px 12px 10px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n/* Render/source segmented toggle, top-left. */\n.fv__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.fv__view-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 26px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--faint);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.fv__view-btn:hover {\n color: var(--muted);\n}\n.fv__view-btn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n.fv__title {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n.fv__fname {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 13px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.fv__dot {\n flex-shrink: 0;\n color: var(--faint);\n}\n.fv__ext {\n flex-shrink: 0;\n font-size: 15px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.fv__actions {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-shrink: 0;\n}\n.fv__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 5px;\n height: 30px;\n min-width: 30px;\n padding: 0 7px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n font-family: inherit;\n font-size: 12px;\n text-decoration: none;\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.fv__btn:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.fv__btn--text {\n padding: 0 9px;\n}\n/* \"open with\" toggle, accented while an override is active (so the forced format reads at a glance). */\n.fv__btn--on {\n color: var(--accent);\n background: var(--surface-2);\n}\n.fv__btn--on:hover {\n color: var(--accent);\n}\n\n/* \"Open with\" dropdown: anchored under its toggle button, right-aligned to the actions edge. */\n.fv__menu {\n position: relative;\n display: inline-flex;\n}\n.fv__menu-pop {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 60;\n min-width: 184px;\n padding: 5px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 10px;\n box-shadow: var(--shadow);\n}\n.fv__menu-item {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 7px 9px;\n background: transparent;\n border: none;\n border-radius: 7px;\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.fv__menu-item:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.fv__menu-item--on {\n color: var(--accent);\n}\n.fv__menu-label {\n flex: 1 1 auto;\n min-width: 0;\n}\n.fv__menu-check {\n flex-shrink: 0;\n color: var(--accent);\n}\n.fv__menu-sep {\n height: 1px;\n margin: 4px 6px;\n background: var(--border);\n}\n\n@media (max-width: 860px) {\n /* Too narrow to split — overlay the viewer full-screen instead. */\n .fv {\n position: fixed;\n inset: 0;\n flex-basis: auto;\n width: 100%;\n z-index: 70;\n }\n .fv__resizer {\n display: none;\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileViewerPanel.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/FileViewerPanel.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useFileViewer, type ViewerFile } from '@/composables/useFileViewer'\nimport { canToggleView, skipsTextLoad } from '@/lib/viewerKind'\nimport { VIEWER_FORMATTERS, findFormatter } from '@/lib/viewerFormats'\nimport { fileExt } from '@/lib/fileType'\nimport FileViewerBody from '@/components/FileViewerBody.vue'\nimport Icon from '@/components/icons/Icon.vue'\n\n/**\n * Right-docked viewer for files (DetailView reflows the conversation left to make room — a split pane, not an\n * overlay). The header owns the title, the render/source toggle, and the copy/download/refresh/close actions;\n * the CONTENT is delegated to {@link FileViewerBody}, the shared type dispatcher (also used by the archive\n * browsers, so a file inside a zip previews with the same viewers). The left edge is a drag splitter.\n */\nconst { current, width, setWidth, close } = useFileViewer()\n\ntype ViewMode = 'render' | 'source'\nconst mode = ref<ViewMode>('render')\nconst text = ref('') // body emits the loaded text up so the copy button can use it\nconst copied = ref(false)\nlet copyTimer: ReturnType<typeof setTimeout> | undefined\nconst bodyRef = ref<{\n reload: () => void\n pickFormat: (key: string) => void\n resetFormat: () => void\n} | null>(null)\n\n// The EFFECTIVE file the body is actually showing — it differs from `current` when the user picks a formatter\n// for an otherwise-unpreviewable file (\"open with\", see FileViewerBody). The body announces it via\n// `formatchange`, so the header's render/source toggle + copy/refresh track the chosen format, not the raw file.\nconst shown = ref<ViewerFile | null>(null)\nconst effective = computed(() => shown.value ?? current.value)\n\n// Inline content (e.g. a loaded skill): rendered directly — no download/refresh, but copy still applies.\nconst isInline = computed(() => current.value?.content != null)\n// A file whose content comes from its own sub-viewer / URL (not the panel-loaded text): no copy/refresh.\nconst selfRendered = computed(() => skipsTextLoad(effective.value))\n// Markdown / html / svg / subtitle / geo / csv / mind-map can flip between rendered and source.\nconst canToggle = computed(() => canToggleView(effective.value, text.value))\n\n// ── \"open with\" menu ──────────────────────────────────────────────────────────────────────────\n// The universal entry point: any file can be force-opened as a different formatter (hex / text / image /\n// audio / …) when auto-detection guessed wrong. The body owns the actual override; this header menu just\n// drives it through the exposed pickFormat / resetFormat. `activeFormat` mirrors the body's current choice\n// (it announces it via `formatpick`), so the menu checks the active row and the button labels it.\nconst formatters = VIEWER_FORMATTERS\nconst menuOpen = ref(false)\nconst menuRef = ref<HTMLElement | null>(null)\nconst activeFormat = ref('')\nconst activeFormatter = computed(() => findFormatter(activeFormat.value))\n// Formatters reinterpret fetched bytes, so they need a URL — hide the menu for inline-only content (no url).\nconst showChooser = computed(() => !!current.value?.url)\nconst chooserTitle = computed(() =>\n activeFormatter.value\n ? `已强制以「${activeFormatter.value.label}」打开 —— 点击切换查看方式`\n : '打开方式 —— 选择查看器(十六进制 / 文本 / 图片 / 音频 …)',\n)\n\nfunction chooseFormat(key: string): void {\n bodyRef.value?.pickFormat(key)\n menuOpen.value = false\n}\nfunction chooseAuto(): void {\n bodyRef.value?.resetFormat()\n menuOpen.value = false\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 menuRef, so a click on either is \"inside\" and never self-closes.\nfunction onDocPointer(e: MouseEvent): void {\n if (menuRef.value && !menuRef.value.contains(e.target as Node)) menuOpen.value = false\n}\nfunction onMenuKey(e: KeyboardEvent): void {\n if (e.key === 'Escape') menuOpen.value = false\n}\nwatch(menuOpen, (on) => {\n if (typeof document === 'undefined') return\n if (on) {\n document.addEventListener('click', onDocPointer)\n document.addEventListener('keydown', onMenuKey)\n } else {\n document.removeEventListener('click', onDocPointer)\n document.removeEventListener('keydown', onMenuKey)\n }\n})\n\n// Header: \"name · EXT\" with the extension split off and emphasized.\nconst ext = computed(() => fileExt(current.value?.name ?? '').toUpperCase())\nconst baseName = computed(() => {\n const n = current.value?.name ?? ''\n const e = fileExt(n)\n return e ? n.slice(0, n.length - e.length - 1) : n\n})\n\n// Re-load whenever a different file is opened (the panel stays mounted across files);\n// each newly opened file starts in its rendered (default) view. The body owns the actual fetch.\nwatch(current, (f) => {\n if (!f) return\n mode.value = 'render'\n text.value = ''\n shown.value = null // body re-announces via `formatchange`; clear so a stale override never lingers\n activeFormat.value = '' // a new file starts auto-detected (the body also re-announces via `formatpick`)\n menuOpen.value = false\n})\n\nasync function copy(): Promise<void> {\n try {\n await navigator.clipboard?.writeText(text.value)\n } catch {\n /* clipboard unavailable — ignore */\n }\n copied.value = true\n clearTimeout(copyTimer)\n copyTimer = setTimeout(() => (copied.value = false), 1400)\n}\n\nconst resizing = ref(false)\n\n// Drag the left edge to resize. The pane is docked right, so moving the cursor left widens it.\n// `resizing` drives a scoped class that colors only the grip — no global body class, so the color\n// cannot leak onto the page. The page-wide resize cursor + selection lock are set inline on <body>.\nfunction onResizeStart(e: PointerEvent): void {\n e.preventDefault()\n const el = e.currentTarget as HTMLElement\n const startX = e.clientX\n const startW = width.value\n const body = document.body\n const prevCursor = body.style.cursor\n const prevSelect = body.style.userSelect\n el.setPointerCapture(e.pointerId)\n resizing.value = true\n body.style.cursor = 'col-resize'\n body.style.userSelect = 'none'\n const move = (ev: PointerEvent) => setWidth(startW + (startX - ev.clientX))\n const noSelect = (ev: Event) => ev.preventDefault()\n const up = (ev: PointerEvent) => {\n el.releasePointerCapture(ev.pointerId)\n el.removeEventListener('pointermove', move)\n el.removeEventListener('pointerup', up)\n document.removeEventListener('selectstart', noSelect)\n body.style.cursor = prevCursor\n body.style.userSelect = prevSelect\n resizing.value = false\n }\n el.addEventListener('pointermove', move)\n el.addEventListener('pointerup', up)\n document.addEventListener('selectstart', noSelect)\n}\n\nonBeforeUnmount(() => {\n clearTimeout(copyTimer)\n if (typeof document !== 'undefined') {\n document.removeEventListener('click', onDocPointer)\n document.removeEventListener('keydown', onMenuKey)\n }\n})\n</script>\n\n<template>\n <aside v-if=\"current\" class=\"fv\">\n <div\n class=\"fv__resizer\"\n :class=\"{ 'fv__resizer--active': resizing }\"\n title=\"拖动调整宽度\"\n @pointerdown=\"onResizeStart\"\n >\n <span class=\"fv__grip\" />\n </div>\n\n <header class=\"fv__head\">\n <!-- markdown / html / svg / …: flip between the rendered view and its source. -->\n <div v-if=\"canToggle\" class=\"fv__view\" role=\"group\" aria-label=\"渲染或源码\">\n <button\n type=\"button\"\n class=\"fv__view-btn\"\n :class=\"{ 'fv__view-btn--on': mode === 'render' }\"\n :aria-pressed=\"mode === 'render'\"\n title=\"渲染\"\n @click=\"mode = 'render'\"\n >\n <Icon name=\"eye\" :size=\"15\" />\n </button>\n <button\n type=\"button\"\n class=\"fv__view-btn\"\n :class=\"{ 'fv__view-btn--on': mode === 'source' }\"\n :aria-pressed=\"mode === 'source'\"\n title=\"源码\"\n @click=\"mode = 'source'\"\n >\n <Icon name=\"code\" :size=\"15\" />\n </button>\n </div>\n <span class=\"fv__title\" :title=\"current.name\">\n <span class=\"fv__fname\">{{ baseName }}</span>\n <template v-if=\"ext\"\n ><span class=\"fv__dot\">·</span><span class=\"fv__ext\">{{ ext }}</span></template\n >\n </span>\n <div class=\"fv__actions\">\n <!-- \"open with\" menu: universal viewer chooser — any file can be force-opened as hex/text/image/audio/…\n when auto-detection guessed wrong. Drives the body's override via the exposed pickFormat/resetFormat. -->\n <div v-if=\"showChooser\" ref=\"menuRef\" class=\"fv__menu\">\n <button\n type=\"button\"\n class=\"fv__btn fv__btn--text\"\n :class=\"{ 'fv__btn--on': activeFormat }\"\n :title=\"chooserTitle\"\n :aria-haspopup=\"true\"\n :aria-expanded=\"menuOpen\"\n @click=\"menuOpen = !menuOpen\"\n >\n <Icon name=\"apps\" :size=\"14\" /><span>{{\n activeFormatter ? activeFormatter.label : '打开方式'\n }}</span>\n </button>\n <div v-if=\"menuOpen\" class=\"fv__menu-pop\" role=\"menu\">\n <button\n type=\"button\"\n class=\"fv__menu-item\"\n :class=\"{ 'fv__menu-item--on': !activeFormat }\"\n role=\"menuitemradio\"\n :aria-checked=\"!activeFormat\"\n @click=\"chooseAuto\"\n >\n <Icon name=\"eye\" :size=\"14\" /><span class=\"fv__menu-label\">自动检测</span>\n <Icon v-if=\"!activeFormat\" name=\"check\" :size=\"15\" class=\"fv__menu-check\" />\n </button>\n <div class=\"fv__menu-sep\" />\n <button\n v-for=\"f in formatters\"\n :key=\"f.key\"\n type=\"button\"\n class=\"fv__menu-item\"\n :class=\"{ 'fv__menu-item--on': activeFormat === f.key }\"\n role=\"menuitemradio\"\n :aria-checked=\"activeFormat === f.key\"\n @click=\"chooseFormat(f.key)\"\n >\n <Icon :name=\"f.icon\" :size=\"14\" /><span class=\"fv__menu-label\">{{ f.label }}</span>\n <Icon v-if=\"activeFormat === f.key\" name=\"check\" :size=\"15\" class=\"fv__menu-check\" />\n </button>\n </div>\n </div>\n <button v-if=\"!selfRendered\" type=\"button\" class=\"fv__btn fv__btn--text\" @click=\"copy\">\n <Icon name=\"copy\" :size=\"14\" /><span>{{ copied ? 'Copied' : 'Copy' }}</span>\n </button>\n <a\n v-if=\"!isInline\"\n class=\"fv__btn\"\n :href=\"current.url\"\n :download=\"current.name\"\n title=\"下载\"\n ><Icon name=\"download\" :size=\"15\"\n /></a>\n <button\n v-if=\"!isInline && !selfRendered\"\n type=\"button\"\n class=\"fv__btn\"\n title=\"刷新\"\n @click=\"bodyRef?.reload()\"\n >\n <Icon name=\"retry\" :size=\"15\" />\n </button>\n <button type=\"button\" class=\"fv__btn\" title=\"关闭\" @click=\"close\">\n <Icon name=\"close\" :size=\"16\" />\n </button>\n </div>\n </header>\n\n <FileViewerBody\n ref=\"bodyRef\"\n :file=\"current\"\n :mode=\"mode\"\n host-chooser\n @loaded=\"text = $event\"\n @formatchange=\"shown = $event\"\n @formatpick=\"activeFormat = $event\"\n />\n </aside>\n</template>\n\n<style scoped>\n/* In-flow flex sibling of the conversation (DetailView is the flex row), so each pane owns its own\n * scrollbar — the conversation's sits at its right edge, not hidden behind a floating overlay. */\n.fv {\n --fv-code-fs: 12.5px;\n --fv-code-lh: 21px;\n position: relative;\n flex: 0 0 var(--fv-width);\n min-width: 0;\n height: 100%;\n z-index: 55;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n border-left: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n/* Left-edge drag handle: a wide invisible hit area with a full-height rail + a small centered grip. */\n.fv__resizer {\n position: absolute;\n top: 0;\n bottom: 0;\n left: -4px;\n width: 11px;\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: col-resize;\n touch-action: none;\n}\n.fv__resizer::before {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n width: 2px;\n transform: translateX(-50%);\n background: transparent;\n transition: background 0.15s;\n}\n.fv__grip {\n position: relative;\n width: 5px;\n height: 34px;\n border-radius: 3px;\n background: var(--border-2);\n transition:\n background 0.15s,\n height 0.15s;\n}\n.fv__resizer:hover::before,\n.fv__resizer--active::before {\n background: #6ea8fe;\n}\n.fv__resizer:hover .fv__grip,\n.fv__resizer--active .fv__grip {\n background: #6ea8fe;\n}\n.fv__resizer--active .fv__grip {\n height: 48px;\n}\n\n.fv__head {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-shrink: 0;\n padding: 10px 12px 10px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n/* Render/source segmented toggle, top-left. */\n.fv__view {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n gap: 2px;\n padding: 2px;\n border-radius: 8px;\n background: var(--surface-2);\n}\n.fv__view-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 26px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 6px;\n color: var(--faint);\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.fv__view-btn:hover {\n color: var(--muted);\n}\n.fv__view-btn--on {\n background: var(--surface);\n color: var(--accent);\n box-shadow: var(--shadow);\n}\n.fv__title {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 5px;\n white-space: nowrap;\n overflow: hidden;\n}\n.fv__fname {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 13px;\n font-weight: 500;\n color: var(--text-strong);\n}\n.fv__dot {\n flex-shrink: 0;\n color: var(--faint);\n}\n.fv__ext {\n flex-shrink: 0;\n font-size: 15px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--muted);\n}\n.fv__actions {\n display: flex;\n align-items: center;\n gap: 2px;\n flex-shrink: 0;\n}\n.fv__btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 5px;\n height: 30px;\n min-width: 30px;\n padding: 0 7px;\n background: transparent;\n border: none;\n border-radius: 7px;\n color: var(--muted);\n font-family: inherit;\n font-size: 12px;\n text-decoration: none;\n cursor: pointer;\n transition:\n background 0.15s,\n color 0.15s;\n}\n.fv__btn:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.fv__btn--text {\n padding: 0 9px;\n}\n/* \"open with\" toggle, accented while an override is active (so the forced format reads at a glance). */\n.fv__btn--on {\n color: var(--accent);\n background: var(--surface-2);\n}\n.fv__btn--on:hover {\n color: var(--accent);\n}\n\n/* \"Open with\" dropdown: anchored under its toggle button, right-aligned to the actions edge. */\n.fv__menu {\n position: relative;\n display: inline-flex;\n}\n.fv__menu-pop {\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n z-index: 60;\n min-width: 184px;\n padding: 5px;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 10px;\n box-shadow: var(--shadow);\n}\n.fv__menu-item {\n display: flex;\n align-items: center;\n gap: 9px;\n width: 100%;\n padding: 7px 9px;\n background: transparent;\n border: none;\n border-radius: 7px;\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.fv__menu-item:hover {\n background: var(--surface-2);\n color: var(--accent);\n}\n.fv__menu-item--on {\n color: var(--accent);\n}\n.fv__menu-label {\n flex: 1 1 auto;\n min-width: 0;\n}\n.fv__menu-check {\n flex-shrink: 0;\n color: var(--accent);\n}\n.fv__menu-sep {\n height: 1px;\n margin: 4px 6px;\n background: var(--border);\n}\n\n@media (max-width: 860px) {\n /* Too narrow to split — overlay the viewer full-screen instead. */\n .fv {\n position: fixed;\n inset: 0;\n flex-basis: auto;\n width: 100%;\n z-index: 70;\n }\n .fv__resizer {\n display: none;\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FontViewer.js","names":[],"sources":["../../src/components/FontViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { formatBytes } from '@/lib/format'\nimport { fileExt } from '@/lib/fileType'\n\n/**\n * Side-viewer font specimen. Fetches the font bytes from the download URL and loads them with the native\n * FontFace API (which decodes ttf/otf/woff AND woff2 in-browser, no library needed), then renders a living\n * specimen in that very face: an editable preview line with a size slider, a size waterfall, and the glyph\n * sets. Mirrors the other byte-fetching viewers (DICOM/DOCX/…): a real download URL in, a self-owned fetch.\n *\n * The face is registered under a unique private alias (so multiple opens never collide) and removed from\n * document.fonts on change/unmount. No styling/security concern: a font is inert data decoded by the browser.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst ready = ref(false)\n\n// Module-level counter gives each loaded face a unique, collision-free CSS family alias (no Math.random).\nlet faceSeq = 0\nlet alias = ''\nlet loadedFace: FontFace | null = null\n\n// Fonts are small; gate only the rare giant file behind a confirm (decoding is in-memory).\nconst SIZE_LIMIT = 24 * 1024 * 1024\n\nconst previewText = ref('The quick brown fox jumps over the lazy dog.')\nconst fontSize = ref(44)\n\nconst WATERFALL = [14, 18, 24, 32, 48, 72]\nconst GLYPH_ROWS = [\n 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',\n 'abcdefghijklmnopqrstuvwxyz',\n '0123456789',\n '!\"#$%&\\'()*+,-./:;<=>?@[\\\\]^_`{|}~',\n '中文示例:永东国爱龙凤,0123456789。',\n]\n\nconst fontFamily = computed(() => (ready.value && alias ? `\"${alias}\"` : 'inherit'))\n\nfunction cleanup(): void {\n if (loadedFace) {\n try {\n document.fonts.delete(loadedFace)\n } catch {\n /* face already gone — ignore */\n }\n loadedFace = null\n }\n ready.value = false\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n cleanup()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n alias = `fv-font-${++faceSeq}`\n const face = new FontFace(alias, buf)\n await face.load()\n document.fonts.add(face)\n loadedFace = face\n ready.value = true\n status.value = 'ready'\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\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(cleanup)\n</script>\n\n<template>\n <div class=\"font\">\n <p v-if=\"status === 'loading'\" class=\"font__msg\">正在加载字体…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"font__msg font__gate\">\n <p>该字体文件较大({{ formatBytes(props.size) }}),将在浏览器内解码。是否继续?</p>\n <button type=\"button\" class=\"font__btn\" @click=\"doLoad\">加载并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"font__msg font__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <p class=\"font__hint\">浏览器可能不支持该字体格式,可尝试下载后用本地工具打开。</p>\n <button type=\"button\" class=\"font__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready'\">\n <!-- controls: editable preview text + size slider, fixed above the scrolling specimen -->\n <div class=\"font__bar\">\n <input\n v-model=\"previewText\"\n class=\"font__input\"\n type=\"text\"\n placeholder=\"输入预览文字…\"\n aria-label=\"预览文字\"\n />\n <label class=\"font__size\">\n <input v-model.number=\"fontSize\" type=\"range\" min=\"8\" max=\"120\" step=\"1\" />\n <span class=\"font__size-val\">{{ fontSize }}px</span>\n </label>\n </div>\n\n <div class=\"font__scroll\">\n <!-- the big editable line, in the loaded face -->\n <div class=\"font__hero\" :style=\"{ fontFamily, fontSize: fontSize + 'px' }\">\n {{ previewText || ' ' }}\n </div>\n\n <!-- size waterfall: the same text at a range of sizes -->\n <section class=\"font__section\">\n <h3 class=\"font__h\">字号瀑布</h3>\n <div\n v-for=\"px in WATERFALL\"\n :key=\"px\"\n class=\"font__wf\"\n :style=\"{ fontFamily, fontSize: px + 'px' }\"\n >\n <span class=\"font__wf-tag\">{{ px }}</span>\n <span class=\"font__wf-text\">{{ previewText || 'Aa' }}</span>\n </div>\n </section>\n\n <!-- glyph sets -->\n <section class=\"font__section\">\n <h3 class=\"font__h\">字符集</h3>\n <p v-for=\"(row, i) in GLYPH_ROWS\" :key=\"i\" class=\"font__glyphs\" :style=\"{ fontFamily }\">\n {{ row }}\n </p>\n </section>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.font {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.font__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.font__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n color: var(--danger);\n}\n.font__hint {\n color: var(--faint);\n font-size: 12px;\n}\n.font__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.font__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.font__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.font__bar {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-shrink: 0;\n padding: 10px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.font__input {\n flex: 1 1 auto;\n min-width: 0;\n padding: 7px 11px;\n font-family: inherit;\n font-size: 13px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n}\n.font__input:focus {\n outline: none;\n border-color: var(--accent);\n}\n.font__size {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n.font__size-val {\n width: 42px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--muted);\n}\n\n.font__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 20px 20px 80px;\n}\n.font__hero {\n line-height: 1.25;\n color: var(--text-strong);\n word-break: break-word;\n padding-bottom: 18px;\n border-bottom: 1px solid var(--border);\n}\n\n.font__section {\n margin-top: 22px;\n}\n.font__h {\n margin: 0 0 12px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--faint);\n}\n.font__wf {\n display: flex;\n align-items: baseline;\n gap: 14px;\n padding: 6px 0;\n line-height: 1.2;\n color: var(--text);\n word-break: break-word;\n}\n.font__wf-tag {\n flex-shrink: 0;\n width: 34px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n}\n.font__wf-text {\n min-width: 0;\n}\n.font__glyphs {\n margin: 0 0 10px;\n font-size: 22px;\n line-height: 1.5;\n color: var(--text);\n word-break: break-all;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FontViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/FontViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { formatBytes } from '@/lib/format'\nimport { fileExt } from '@/lib/fileType'\n\n/**\n * Side-viewer font specimen. Fetches the font bytes from the download URL and loads them with the native\n * FontFace API (which decodes ttf/otf/woff AND woff2 in-browser, no library needed), then renders a living\n * specimen in that very face: an editable preview line with a size slider, a size waterfall, and the glyph\n * sets. Mirrors the other byte-fetching viewers (DICOM/DOCX/…): a real download URL in, a self-owned fetch.\n *\n * The face is registered under a unique private alias (so multiple opens never collide) and removed from\n * document.fonts on change/unmount. No styling/security concern: a font is inert data decoded by the browser.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst ready = ref(false)\n\n// Module-level counter gives each loaded face a unique, collision-free CSS family alias (no Math.random).\nlet faceSeq = 0\nlet alias = ''\nlet loadedFace: FontFace | null = null\n\n// Fonts are small; gate only the rare giant file behind a confirm (decoding is in-memory).\nconst SIZE_LIMIT = 24 * 1024 * 1024\n\nconst previewText = ref('The quick brown fox jumps over the lazy dog.')\nconst fontSize = ref(44)\n\nconst WATERFALL = [14, 18, 24, 32, 48, 72]\nconst GLYPH_ROWS = [\n 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',\n 'abcdefghijklmnopqrstuvwxyz',\n '0123456789',\n '!\"#$%&\\'()*+,-./:;<=>?@[\\\\]^_`{|}~',\n '中文示例:永东国爱龙凤,0123456789。',\n]\n\nconst fontFamily = computed(() => (ready.value && alias ? `\"${alias}\"` : 'inherit'))\n\nfunction cleanup(): void {\n if (loadedFace) {\n try {\n document.fonts.delete(loadedFace)\n } catch {\n /* face already gone — ignore */\n }\n loadedFace = null\n }\n ready.value = false\n}\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n cleanup()\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n alias = `fv-font-${++faceSeq}`\n const face = new FontFace(alias, buf)\n await face.load()\n document.fonts.add(face)\n loadedFace = face\n ready.value = true\n status.value = 'ready'\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\nwatch(() => props.url, load, { immediate: true })\nonBeforeUnmount(cleanup)\n</script>\n\n<template>\n <div class=\"font\">\n <p v-if=\"status === 'loading'\" class=\"font__msg\">正在加载字体…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"font__msg font__gate\">\n <p>该字体文件较大({{ formatBytes(props.size) }}),将在浏览器内解码。是否继续?</p>\n <button type=\"button\" class=\"font__btn\" @click=\"doLoad\">加载并预览</button>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"font__msg font__msg--error\">\n <p>加载失败:{{ errorMsg }}</p>\n <p class=\"font__hint\">浏览器可能不支持该字体格式,可尝试下载后用本地工具打开。</p>\n <button type=\"button\" class=\"font__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <template v-else-if=\"status === 'ready'\">\n <!-- controls: editable preview text + size slider, fixed above the scrolling specimen -->\n <div class=\"font__bar\">\n <input\n v-model=\"previewText\"\n class=\"font__input\"\n type=\"text\"\n placeholder=\"输入预览文字…\"\n aria-label=\"预览文字\"\n />\n <label class=\"font__size\">\n <input v-model.number=\"fontSize\" type=\"range\" min=\"8\" max=\"120\" step=\"1\" />\n <span class=\"font__size-val\">{{ fontSize }}px</span>\n </label>\n </div>\n\n <div class=\"font__scroll\">\n <!-- the big editable line, in the loaded face -->\n <div class=\"font__hero\" :style=\"{ fontFamily, fontSize: fontSize + 'px' }\">\n {{ previewText || ' ' }}\n </div>\n\n <!-- size waterfall: the same text at a range of sizes -->\n <section class=\"font__section\">\n <h3 class=\"font__h\">字号瀑布</h3>\n <div\n v-for=\"px in WATERFALL\"\n :key=\"px\"\n class=\"font__wf\"\n :style=\"{ fontFamily, fontSize: px + 'px' }\"\n >\n <span class=\"font__wf-tag\">{{ px }}</span>\n <span class=\"font__wf-text\">{{ previewText || 'Aa' }}</span>\n </div>\n </section>\n\n <!-- glyph sets -->\n <section class=\"font__section\">\n <h3 class=\"font__h\">字符集</h3>\n <p v-for=\"(row, i) in GLYPH_ROWS\" :key=\"i\" class=\"font__glyphs\" :style=\"{ fontFamily }\">\n {{ row }}\n </p>\n </section>\n </div>\n </template>\n </div>\n</template>\n\n<style scoped>\n.font {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n}\n\n.font__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.font__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n color: var(--danger);\n}\n.font__hint {\n color: var(--faint);\n font-size: 12px;\n}\n.font__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 12px;\n}\n.font__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.font__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.font__bar {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-shrink: 0;\n padding: 10px 16px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.font__input {\n flex: 1 1 auto;\n min-width: 0;\n padding: 7px 11px;\n font-family: inherit;\n font-size: 13px;\n color: var(--text);\n background: var(--code-bg);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n}\n.font__input:focus {\n outline: none;\n border-color: var(--accent);\n}\n.font__size {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n.font__size-val {\n width: 42px;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--muted);\n}\n\n.font__scroll {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n padding: 20px 20px 80px;\n}\n.font__hero {\n line-height: 1.25;\n color: var(--text-strong);\n word-break: break-word;\n padding-bottom: 18px;\n border-bottom: 1px solid var(--border);\n}\n\n.font__section {\n margin-top: 22px;\n}\n.font__h {\n margin: 0 0 12px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--faint);\n}\n.font__wf {\n display: flex;\n align-items: baseline;\n gap: 14px;\n padding: 6px 0;\n line-height: 1.2;\n color: var(--text);\n word-break: break-word;\n}\n.font__wf-tag {\n flex-shrink: 0;\n width: 34px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--faint);\n}\n.font__wf-text {\n min-width: 0;\n}\n.font__glyphs {\n margin: 0 0 10px;\n font-size: 22px;\n line-height: 1.5;\n color: var(--text);\n word-break: break-all;\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FrontmatterCard.js","names":[],"sources":["../../src/components/FrontmatterCard.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * Renders a markdown document's YAML frontmatter (SKILL.md & friends) as a labeled\n * card — one \"Label / value\" row per key, claude.ai's skill-header look. `name` is\n * dropped (it's already the panel title); empty values are skipped.\n */\nconst props = defineProps<{ data: Record<string, unknown> }>()\n\n// \"allowed-tools\" / \"allowed_tools\" → \"Allowed tools\"\nfunction humanize(key: string): string {\n const s = key.replace(/[-_]+/g, ' ').trim()\n return s ? s.charAt(0).toUpperCase() + s.slice(1) : key\n}\n\nfunction toText(v: unknown): string {\n if (v == null) return ''\n if (Array.isArray(v)) return v.map(toText).filter(Boolean).join(', ')\n if (typeof v === 'object') {\n return Object.entries(v as Record<string, unknown>)\n .map(([k, val]) => `${k}: ${toText(val)}`)\n .join('\\n')\n }\n return String(v)\n}\n\nconst rows = computed(() =>\n Object.entries(props.data)\n .filter(([k]) => k !== 'name')\n .map(([k, v]) => ({ key: k, label: humanize(k), value: toText(v) }))\n .filter((r) => r.value.trim() !== ''),\n)\n</script>\n\n<template>\n <dl v-if=\"rows.length\" class=\"fm\">\n <div v-for=\"r in rows\" :key=\"r.key\" class=\"fm__row\">\n <dt class=\"fm__label\">{{ r.label }}</dt>\n <dd class=\"fm__value\">{{ r.value }}</dd>\n </div>\n </dl>\n</template>\n\n<style scoped>\n.fm {\n margin: 0 0 24px;\n padding: 0 0 18px;\n border-bottom: 1px solid var(--border);\n}\n.fm__row {\n margin: 0 0 16px;\n}\n.fm__row:last-child {\n margin-bottom: 0;\n}\n.fm__label {\n margin: 0 0 4px;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.01em;\n color: var(--faint);\n}\n.fm__value {\n margin: 0;\n font-family: var(--font-serif);\n font-size: 15px;\n line-height: 1.7;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n</style>\n"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FrontmatterCard.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/FrontmatterCard.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * Renders a markdown document's YAML frontmatter (SKILL.md & friends) as a labeled\n * card — one \"Label / value\" row per key, claude.ai's skill-header look. `name` is\n * dropped (it's already the panel title); empty values are skipped.\n */\nconst props = defineProps<{ data: Record<string, unknown> }>()\n\n// \"allowed-tools\" / \"allowed_tools\" → \"Allowed tools\"\nfunction humanize(key: string): string {\n const s = key.replace(/[-_]+/g, ' ').trim()\n return s ? s.charAt(0).toUpperCase() + s.slice(1) : key\n}\n\nfunction toText(v: unknown): string {\n if (v == null) return ''\n if (Array.isArray(v)) return v.map(toText).filter(Boolean).join(', ')\n if (typeof v === 'object') {\n return Object.entries(v as Record<string, unknown>)\n .map(([k, val]) => `${k}: ${toText(val)}`)\n .join('\\n')\n }\n return String(v)\n}\n\nconst rows = computed(() =>\n Object.entries(props.data)\n .filter(([k]) => k !== 'name')\n .map(([k, v]) => ({ key: k, label: humanize(k), value: toText(v) }))\n .filter((r) => r.value.trim() !== ''),\n)\n</script>\n\n<template>\n <dl v-if=\"rows.length\" class=\"fm\">\n <div v-for=\"r in rows\" :key=\"r.key\" class=\"fm__row\">\n <dt class=\"fm__label\">{{ r.label }}</dt>\n <dd class=\"fm__value\">{{ r.value }}</dd>\n </div>\n </dl>\n</template>\n\n<style scoped>\n.fm {\n margin: 0 0 24px;\n padding: 0 0 18px;\n border-bottom: 1px solid var(--border);\n}\n.fm__row {\n margin: 0 0 16px;\n}\n.fm__row:last-child {\n margin-bottom: 0;\n}\n.fm__label {\n margin: 0 0 4px;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.01em;\n color: var(--faint);\n}\n.fm__value {\n margin: 0;\n font-family: var(--font-serif);\n font-size: 15px;\n line-height: 1.7;\n color: var(--text);\n white-space: pre-wrap;\n word-break: break-word;\n}\n</style>\n"],"mappings":";;;;;;;;;EAQA,IAAM,IAAQ;EAGd,SAAS,EAAS,GAAqB;GACrC,IAAM,IAAI,EAAI,QAAQ,UAAU,GAAG,CAAC,CAAC,KAAK;GAC1C,OAAO,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,EAAE,MAAM,CAAC,IAAI;EACtD;EAEA,SAAS,EAAO,GAAoB;GAQlC,OAPI,KAAK,OAAa,KAClB,MAAM,QAAQ,CAAC,IAAU,EAAE,IAAI,CAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,IAAI,IAChE,OAAO,KAAM,WACR,OAAO,QAAQ,CAA4B,CAAA,CAC/C,KAAK,CAAC,GAAG,OAAS,GAAG,EAAE,IAAI,EAAO,CAAG,GAAG,CAAA,CACxC,KAAK,IAAI,IAEP,OAAO,CAAC;EACjB;EAEA,IAAM,IAAO,QACX,OAAO,QAAQ,EAAM,IAAI,CAAA,CACtB,QAAQ,CAAC,OAAO,MAAM,MAAM,CAAA,CAC5B,KAAK,CAAC,GAAG,QAAQ;GAAE,KAAK;GAAG,OAAO,EAAS,CAAC;GAAG,OAAO,EAAO,CAAC;EAAE,EAAE,CAAA,CAClE,QAAQ,MAAM,EAAE,MAAM,KAAK,MAAM,EAAE,CACxC;mBAIY,EAAA,MAAK,UAAA,EAAA,GAAf,EAKK,MALL,GAKK,EAAA,EAAA,EAAA,GAJH,EAGM,GAAA,MAAA,EAHW,EAAA,QAAL,YAAZ,EAGM,OAAA;GAHkB,KAAK,EAAE;GAAK,OAAM;MACxC,EAAwC,MAAxC,GAAwC,EAAf,EAAE,KAAK,GAAA,CAAA,GAChC,EAAwC,MAAxC,GAAwC,EAAf,EAAE,KAAK,GAAA,CAAA,CAAA,CAAA"}
|