@brftech/filex-core 0.44.1 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ArchiveViewer-D_tJxJhJ.js → ArchiveViewer-QuNXAECN.js} +2 -2
- package/dist/{ArchiveViewer-D_tJxJhJ.js.map → ArchiveViewer-QuNXAECN.js.map} +1 -1
- package/dist/{CsvViewer-B95fArq0.js → CsvViewer-niuoz1Uk.js} +2 -2
- package/dist/{CsvViewer-B95fArq0.js.map → CsvViewer-niuoz1Uk.js.map} +1 -1
- package/dist/{DrawioViewer-BAaPMTge.js → DrawioViewer-BtqMiasr.js} +2 -2
- package/dist/{DrawioViewer-BAaPMTge.js.map → DrawioViewer-BtqMiasr.js.map} +1 -1
- package/dist/{EpubViewer-B-B7bRW7.js → EpubViewer-ERm7L1WN.js} +2 -2
- package/dist/{EpubViewer-B-B7bRW7.js.map → EpubViewer-ERm7L1WN.js.map} +1 -1
- package/dist/{IpynbViewer-COOTyOcu.js → IpynbViewer-Cmz5n0VR.js} +2 -2
- package/dist/{IpynbViewer-COOTyOcu.js.map → IpynbViewer-Cmz5n0VR.js.map} +1 -1
- package/dist/{MermaidViewer-CjVCmR3s.js → MermaidViewer-BJ5uOyhc.js} +2 -2
- package/dist/{MermaidViewer-CjVCmR3s.js.map → MermaidViewer-BJ5uOyhc.js.map} +1 -1
- package/dist/{PsdViewer-xsIYL9Gu.js → PsdViewer-DYCJF2BM.js} +2 -2
- package/dist/{PsdViewer-xsIYL9Gu.js.map → PsdViewer-DYCJF2BM.js.map} +1 -1
- package/dist/{TiffViewer-CtwnTxqL.js → TiffViewer-DbJv_0El.js} +2 -2
- package/dist/{TiffViewer-CtwnTxqL.js.map → TiffViewer-DbJv_0El.js.map} +1 -1
- package/dist/{Viewer3D-xYMwndEy.js → Viewer3D-DwZfYoO3.js} +2 -2
- package/dist/{Viewer3D-xYMwndEy.js.map → Viewer3D-DwZfYoO3.js.map} +1 -1
- package/dist/filex-core.js +298 -291
- package/dist/filex-core.umd.cjs +61 -61
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/{index-BBToAjwL.js → index-1RaeWGFO.js} +14218 -13831
- package/dist/index-1RaeWGFO.js.map +1 -0
- package/dist/index.d.ts +253 -31
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +265 -99
- package/src/components/AdvancedSearch.vue +3 -3
- package/src/components/CommandPalette.vue +166 -42
- package/src/components/ConnectionsPanel.vue +3 -11
- package/src/components/SideNav.vue +16 -11
- package/src/components/plugin/PluginViewModal.vue +7 -0
- package/src/components/plugin/nodes/SurfacePdfFields.vue +16 -1
- package/src/components/public/PublicLinkPage.vue +6 -5
- package/src/composables/useFileApi.ts +32 -1
- package/src/composables/usePluginSurface.ts +12 -0
- package/src/composables/useSystemDark.ts +32 -0
- package/src/index.ts +16 -0
- package/src/lib/connectionGuides.ts +11 -15
- package/src/lib/downloadSelection.ts +34 -0
- package/src/lib/dragOut.ts +0 -0
- package/src/lib/searchHit.ts +129 -0
- package/src/lib/storageLine.ts +124 -0
- package/src/lib/webdavMount.ts +120 -0
- package/src/locales/en.ts +13 -0
- package/src/locales/tr.ts +13 -0
- package/src/styles/base.css +84 -7
- package/src/types/ExplorerConfig.ts +65 -1
- package/src/types/Plugins.ts +12 -0
- package/dist/index-BBToAjwL.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent as q, ref as h, computed as p, onMounted as F, watch as J, openBlock as v, createElementBlock as d, createElementVNode as s, unref as H, toDisplayString as o, Fragment as z, renderList as Q, createVNode as M, withCtx as G } from "vue";
|
|
2
|
-
import { u as K, f as $, a as D, _ as X, d as Y } from "./index-
|
|
2
|
+
import { u as K, f as $, a as D, _ as X, d as Y } from "./index-1RaeWGFO.js";
|
|
3
3
|
const Z = { class: "filex-viewer-archive" }, ee = {
|
|
4
4
|
key: 0,
|
|
5
5
|
class: "filex-viewer-fallback"
|
|
@@ -233,4 +233,4 @@ const Z = { class: "filex-viewer-archive" }, ee = {
|
|
|
233
233
|
export {
|
|
234
234
|
we as default
|
|
235
235
|
};
|
|
236
|
-
//# sourceMappingURL=ArchiveViewer-
|
|
236
|
+
//# sourceMappingURL=ArchiveViewer-QuNXAECN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArchiveViewer-D_tJxJhJ.js","sources":["../src/viewers/ArchiveViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * ArchiveViewer — read-only preview of an archive's contents, browsed like a\n * folder (a breadcrumb, folders first), for every format the server reads.\n *\n * Hits the configured archive-list endpoint (`archiveListUrl`, default\n * `POST /api/files/archive/list`) with the adapter-qualified path. An\n * encrypted archive answers PASSWORD_REQUIRED / BAD_PASSWORD and the shared\n * password dialog asks; the listing it unlocks is remembered for the session\n * (`archivePreviewCache`). Extraction is exposed elsewhere (context menu /\n * actions panel): the viewer's job is just \"what's inside?\".\n */\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\nimport DataTable, { type DataColumn } from '../components/DataTable.vue';\nimport { useLocale } from '../composables/useLocale';\nimport type { LocaleCode } from '../types/ExplorerConfig';\nimport type { ArchivePreviewCache } from '../lib/archivePreviewCache';\nimport ArchivePasswordModal from '../modals/ArchivePasswordModal.vue';\n\ninterface ArchiveEntry {\n name: string;\n size: number;\n mtime?: string;\n is_dir?: boolean;\n}\n\ninterface ArchiveRow extends ArchiveEntry {\n displayName: string;\n fullPath: string;\n}\n\nconst props = withDefaults(defineProps<{\n url: string;\n filePath?: string;\n ext: string;\n /** The interface language — the size column is written the way it writes\n * numbers (\"1,96 KB\"), like every other size in the product. */\n locale?: LocaleCode;\n t?: (key: string, vars?: Record<string, string | number>) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n /** ⚠ Where to ask. This used to be the literal '/api/files/archive/list',\n * which is the ONE thing an embed cannot assume: with `apiBase` pointing at\n * another origin the request went to the host page instead and 404'd, while\n * every other call in the package honoured the configured base. The default\n * keeps a same-origin install working unchanged. */\n archiveListUrl?: string;\n archivePreviewCache?: ArchivePreviewCache;\n}>(), {\n locale: 'en',\n});\n\nconst emit = defineEmits<{ (e: 'close'): void }>();\nconst entries = ref<ArchiveEntry[]>([]);\nconst currentDir = ref('');\nconst loading = ref(true);\nconst error = ref<string | null>(null);\nconst password = ref('');\nconst passwordNeeded = ref(false);\nconst passwordError = ref('');\n\n/** ⚠ `vars` go THROUGH `t()`, not into a `.replace()` afterwards: `t()` is what\n * picks the singular (`viewer.archive.entries_one`) from the count, and it can\n * only do that if it is told the count. Same shape CsvViewer's helper has. */\nfunction tt(key: string, fallback: string, vars?: Record<string, string | number>): string {\n return props.t ? props.t(key, vars) : fallback;\n}\n\n/* ⚠ The product's one byte formatter (useLocale.formatSize). This file had\n its own — base 1024, a dot, English units — so a zip's member read\n \"1.9 KB\" beside the explorer's \"1,96 KB\" for the same bytes (QA,\n 2026-09-21). */\nconst { formatSize } = useLocale(() => props.locale ?? 'en');\nfunction fmtSize(n: number): string {\n return formatSize(n);\n}\n\nasync function load(): Promise<void> {\n loading.value = true;\n error.value = null;\n passwordError.value = '';\n entries.value = [];\n if (!props.filePath) {\n error.value = tt('viewer.archive.error', 'Could not read archive contents.');\n loading.value = false;\n return;\n }\n if (!password.value) {\n const cached = props.archivePreviewCache?.recall(props.filePath);\n if (cached) {\n entries.value = cached;\n passwordNeeded.value = false;\n loading.value = false;\n return;\n }\n }\n try {\n const res = await fetch(props.archiveListUrl || '/api/files/archive/list', {\n method: 'POST',\n credentials: props.authCredentials || 'same-origin',\n headers: {\n 'Content-Type': 'application/json',\n ...(props.authHeaders ? await props.authHeaders() : {}),\n },\n body: JSON.stringify({ path: props.filePath, password: password.value || undefined }),\n });\n if (!res.ok) {\n /* ⚠ Never the raw status. This printed \"503 Service Unavailable\" (or a\n * JSON body) straight into the preview — the same failure the owner\n * reported for ONLYOFFICE (\"Config fetch 503: {…}\"): a person is shown\n * the HTTP layer instead of what happened. The server's reason, when it\n * sent one we know, becomes a sentence; anything else is the generic\n * \"could not read the archive\". */\n const body = await res.json().catch(() => ({})) as { code?: string; error?: string };\n if (body.code === 'PASSWORD_REQUIRED' || body.code === 'BAD_PASSWORD') {\n props.archivePreviewCache?.forget(props.filePath);\n passwordNeeded.value = true;\n passwordError.value = body.code === 'BAD_PASSWORD'\n ? tt('archive.bad_password', 'The archive password is incorrect.')\n : '';\n return;\n }\n throw new ArchiveError(archiveErrorText(res.status, body.code ?? body.error ?? ''));\n }\n const body = (await res.json()) as { entries?: ArchiveEntry[] };\n entries.value = body.entries ?? [];\n if (password.value) props.archivePreviewCache?.remember(props.filePath, entries.value);\n passwordNeeded.value = false;\n } catch (err) {\n error.value =\n err instanceof ArchiveError ? err.message : tt('viewer.archive.error', 'Could not read archive contents.');\n } finally {\n loading.value = false;\n }\n}\n\n/** A failure already worded for a person. Anything else that throws (the\n * network, a JSON parse) reads as the generic sentence. */\nclass ArchiveError extends Error {}\n\nfunction archiveErrorText(status: number, code: string): string {\n if (/not a zip|unsupported|not supported|format/i.test(code)) {\n return tt('viewer.archive.unsupported', 'This kind of archive cannot be listed here. Download it to see what is inside.');\n }\n if (status === 413 || /too large|too big/i.test(code)) {\n return tt('viewer.archive.too_large', 'This archive is too large to list here. Download it to see what is inside.');\n }\n return tt('viewer.archive.error', 'Could not read archive contents.');\n}\n\n\n/**\n * The entries, in THE table (DataTable — the explorer's own). ⚠ It used to be\n * the one table in the package allowed to be its own, with the reasoning that\n * a file's contents answer to the viewer's layout. The owner's rule since\n * 2026-09-21 has no such exception — \"bir yere tablo gerekiyorsa bu tabloyu\n * koymak zorundayız\" — and an archive's listing is a file listing: it wants\n * to be sorted by size and have its Name column widened exactly the way the\n * folder it came from does.\n */\nconst columns = computed<DataColumn<ArchiveRow>[]>(() => [\n {\n id: 'name',\n label: tt('viewer.name', 'Name'),\n sortable: true,\n width: 360,\n class: 'filex-viewer-archive__name',\n title: (e) => e.name,\n },\n {\n id: 'size',\n label: tt('viewer.size', 'Size'),\n sortable: true,\n width: 110,\n align: 'right',\n class: 'filex-viewer-archive__size',\n format: (e) => (e.is_dir ? '' : fmtSize(e.size)),\n sortValue: (e) => (e.is_dir ? -1 : e.size),\n },\n]);\n\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nfunction submitPassword(value: string) {\n password.value = value;\n void load();\n}\n\nonMounted(load);\nwatch(() => props.filePath, () => {\n currentDir.value = '';\n password.value = '';\n passwordNeeded.value = false;\n void load();\n});\n\nconst totalSize = computed(() => entries.value.reduce((sum, entry) => sum + (entry.size || 0), 0));\nconst fileCount = computed(() => entries.value.filter((entry) => !entry.is_dir).length);\n\nfunction cleanPath(value: string): string {\n return value.replace(/\\\\/g, '/').replace(/^\\/+|\\/+$/g, '');\n}\n\nconst visibleEntries = computed<ArchiveRow[]>(() => {\n const rows = new Map<string, ArchiveRow>();\n for (const entry of entries.value) {\n const fullPath = cleanPath(entry.name);\n if (!fullPath || (currentDir.value && !fullPath.startsWith(currentDir.value))) continue;\n const rest = currentDir.value ? fullPath.slice(currentDir.value.length) : fullPath;\n if (!rest) continue;\n const slash = rest.indexOf('/');\n if (slash >= 0) {\n const displayName = rest.slice(0, slash);\n const dirPath = `${currentDir.value}${displayName}`;\n if (!rows.has(dirPath)) {\n rows.set(dirPath, { name: dirPath, fullPath: dirPath, displayName, size: 0, is_dir: true });\n }\n continue;\n }\n rows.set(fullPath, { ...entry, name: fullPath, fullPath, displayName: rest });\n }\n return [...rows.values()].sort((a, b) => {\n if (!!a.is_dir !== !!b.is_dir) return a.is_dir ? -1 : 1;\n return a.displayName.localeCompare(b.displayName, undefined, { numeric: true, sensitivity: 'base' });\n });\n});\n\nconst breadcrumbs = computed(() => {\n const parts = cleanPath(currentDir.value).split('/').filter(Boolean);\n return parts.map((name, index) => ({ name, path: `${parts.slice(0, index + 1).join('/')}/` }));\n});\n\nfunction openRow(row: ArchiveRow) {\n if (row.is_dir) currentDir.value = `${cleanPath(row.fullPath)}/`;\n\n}\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n\nfunction entryTile(entry: ArchiveEntry): string {\n const name = entry.name || '';\n const dot = name.lastIndexOf('.');\n return fileIconTile({\n type: entry.is_dir ? 'dir' : 'file',\n extension: dot > 0 ? name.slice(dot + 1) : '',\n basename: name,\n });\n}\n</script>\n\n<template>\n <div class=\"filex-viewer-archive\">\n <div v-if=\"passwordNeeded\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"actionIconSvg('lock')\"></span>\n <p>{{ tt('archive.password_required', 'This archive requires a password.') }}</p>\n </div>\n <div v-else-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <div v-else-if=\"loading\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span\n class=\"filex-viewer-fallback__icon filex-viewer-fallback__icon--spin\"\n aria-hidden=\"true\"\n v-html=\"actionIconSvg('progress')\"\n ></span>\n <p>{{ tt('viewer.loading', 'Loading…') }}</p>\n </div>\n <div v-else-if=\"entries.length === 0\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ tt('viewer.archive.empty', 'Archive is empty.') }}</p>\n </div>\n <div v-else class=\"filex-viewer-archive__pane\">\n <nav class=\"filex-viewer-archive__crumbs\" :aria-label=\"tt('viewer.archive.location', 'Archive location')\">\n <button type=\"button\" @click=\"currentDir = ''\">{{ tt('viewer.archive.root', 'Archive') }}</button>\n <template v-for=\"crumb in breadcrumbs\" :key=\"crumb.path\">\n <span aria-hidden=\"true\">›</span>\n <button type=\"button\" @click=\"currentDir = crumb.path\">{{ crumb.name }}</button>\n </template>\n </nav>\n <div class=\"filex-viewer-archive__summary\">\n <!-- ⚠ The count is IN the message (\"{n} files\"). A `{{ fileCount }}`\n printed in front of it read \"3 3 files\". -->\n {{ tt('viewer.archive.entries', `${fileCount} files`, { n: fileCount }) }}\n · {{ fmtSize(totalSize) }}\n </div>\n <DataTable\n table-id=\"viewer.archive\"\n :columns=\"columns\"\n :rows=\"visibleEntries\"\n :row-key=\"(e: ArchiveRow) => e.fullPath\"\n :row-attrs=\"(e: ArchiveRow) => ({ 'data-dir': e.is_dir ? '1' : '0', onDblclick: () => openRow(e) })\"\n >\n <template #cell-name=\"{ row }\">\n <!-- ikon:emoji — the same tile the listing draws for the same kind\n of file, so an archive's contents and the folder it came from\n read as one product. -->\n <button type=\"button\" class=\"filex-viewer-archive__entry\" :disabled=\"!row.is_dir\" @click=\"openRow(row)\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->\n <span class=\"filex-viewer-archive__icon\" aria-hidden=\"true\" v-html=\"entryTile(row)\"></span>\n <span class=\"fe-list__cell-text\">{{ row.displayName }}</span>\n </button>\n </template>\n </DataTable>\n </div>\n <ArchivePasswordModal\n :open=\"passwordNeeded\"\n :locale=\"props.locale\"\n :archive-name=\"(props.filePath || '').split('/').pop() || ''\"\n :busy=\"loading\"\n :error=\"passwordError\"\n @close=\"emit('close')\"\n @submit=\"submitPassword\"\n />\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-archive {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n}\n.filex-viewer-archive__pane {\n flex: 1;\n overflow: auto;\n padding: 16px 20px;\n}\n.filex-viewer-archive__summary {\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n margin-bottom: 12px;\n font-variant-numeric: tabular-nums;\n}\n/* The table itself is DataTable's (styles/base.css); only the viewer's own\n touches stay here — tabular figures and muted sizes, a folder row read as\n quieter than a file. */\n.filex-viewer-archive__size {\n text-align: end;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-archive__icon {\n margin-inline-end: 6px;\n}\n.filex-viewer-archive .fe-list__row[data-dir=\"1\"] {\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n.filex-viewer-archive__crumbs { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; color: var(--fe-text-muted, #5a6475); }\n.filex-viewer-archive__crumbs button { border: 0; background: transparent; color: inherit; cursor: pointer; padding: 2px 4px; border-radius: 4px; }\n.filex-viewer-archive__crumbs button:hover { background: var(--fe-bg-elev, #f7f8fa); color: var(--fe-text, #1a1e27); }\n.filex-viewer-archive__entry { display: inline-flex; align-items: center; max-width: 100%; border: 0; background: transparent; color: inherit; font: inherit; padding: 0; text-align: start; }\n.filex-viewer-archive__entry:not(:disabled) { cursor: pointer; }\n.filex-viewer-archive__entry:disabled { opacity: 1; }\n</style>\n"],"names":["props","__props","emit","__emit","entries","ref","currentDir","loading","error","password","passwordNeeded","passwordError","tt","key","fallback","vars","formatSize","useLocale","fmtSize","n","load","cached","_a","res","body","_b","ArchiveError","archiveErrorText","_c","err","status","code","columns","computed","submitPassword","value","onMounted","watch","totalSize","sum","entry","fileCount","cleanPath","visibleEntries","rows","fullPath","rest","slash","displayName","dirPath","b","breadcrumbs","parts","name","index","openRow","row","typeTile","fileIconTile","entryTile","dot","_openBlock","_createElementBlock","_hoisted_1","_hoisted_2","_createElementVNode","_unref","actionIconSvg","_hoisted_4","_hoisted_6","_hoisted_8","_hoisted_10","_Fragment","_renderList","crumb","_cache","$event","_toDisplayString","_hoisted_12","_hoisted_13","_createVNode","DataTable","e","_withCtx","_hoisted_16","ArchivePasswordModal"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ArchiveViewer-QuNXAECN.js","sources":["../src/viewers/ArchiveViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * ArchiveViewer — read-only preview of an archive's contents, browsed like a\n * folder (a breadcrumb, folders first), for every format the server reads.\n *\n * Hits the configured archive-list endpoint (`archiveListUrl`, default\n * `POST /api/files/archive/list`) with the adapter-qualified path. An\n * encrypted archive answers PASSWORD_REQUIRED / BAD_PASSWORD and the shared\n * password dialog asks; the listing it unlocks is remembered for the session\n * (`archivePreviewCache`). Extraction is exposed elsewhere (context menu /\n * actions panel): the viewer's job is just \"what's inside?\".\n */\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\nimport DataTable, { type DataColumn } from '../components/DataTable.vue';\nimport { useLocale } from '../composables/useLocale';\nimport type { LocaleCode } from '../types/ExplorerConfig';\nimport type { ArchivePreviewCache } from '../lib/archivePreviewCache';\nimport ArchivePasswordModal from '../modals/ArchivePasswordModal.vue';\n\ninterface ArchiveEntry {\n name: string;\n size: number;\n mtime?: string;\n is_dir?: boolean;\n}\n\ninterface ArchiveRow extends ArchiveEntry {\n displayName: string;\n fullPath: string;\n}\n\nconst props = withDefaults(defineProps<{\n url: string;\n filePath?: string;\n ext: string;\n /** The interface language — the size column is written the way it writes\n * numbers (\"1,96 KB\"), like every other size in the product. */\n locale?: LocaleCode;\n t?: (key: string, vars?: Record<string, string | number>) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n /** ⚠ Where to ask. This used to be the literal '/api/files/archive/list',\n * which is the ONE thing an embed cannot assume: with `apiBase` pointing at\n * another origin the request went to the host page instead and 404'd, while\n * every other call in the package honoured the configured base. The default\n * keeps a same-origin install working unchanged. */\n archiveListUrl?: string;\n archivePreviewCache?: ArchivePreviewCache;\n}>(), {\n locale: 'en',\n});\n\nconst emit = defineEmits<{ (e: 'close'): void }>();\nconst entries = ref<ArchiveEntry[]>([]);\nconst currentDir = ref('');\nconst loading = ref(true);\nconst error = ref<string | null>(null);\nconst password = ref('');\nconst passwordNeeded = ref(false);\nconst passwordError = ref('');\n\n/** ⚠ `vars` go THROUGH `t()`, not into a `.replace()` afterwards: `t()` is what\n * picks the singular (`viewer.archive.entries_one`) from the count, and it can\n * only do that if it is told the count. Same shape CsvViewer's helper has. */\nfunction tt(key: string, fallback: string, vars?: Record<string, string | number>): string {\n return props.t ? props.t(key, vars) : fallback;\n}\n\n/* ⚠ The product's one byte formatter (useLocale.formatSize). This file had\n its own — base 1024, a dot, English units — so a zip's member read\n \"1.9 KB\" beside the explorer's \"1,96 KB\" for the same bytes (QA,\n 2026-09-21). */\nconst { formatSize } = useLocale(() => props.locale ?? 'en');\nfunction fmtSize(n: number): string {\n return formatSize(n);\n}\n\nasync function load(): Promise<void> {\n loading.value = true;\n error.value = null;\n passwordError.value = '';\n entries.value = [];\n if (!props.filePath) {\n error.value = tt('viewer.archive.error', 'Could not read archive contents.');\n loading.value = false;\n return;\n }\n if (!password.value) {\n const cached = props.archivePreviewCache?.recall(props.filePath);\n if (cached) {\n entries.value = cached;\n passwordNeeded.value = false;\n loading.value = false;\n return;\n }\n }\n try {\n const res = await fetch(props.archiveListUrl || '/api/files/archive/list', {\n method: 'POST',\n credentials: props.authCredentials || 'same-origin',\n headers: {\n 'Content-Type': 'application/json',\n ...(props.authHeaders ? await props.authHeaders() : {}),\n },\n body: JSON.stringify({ path: props.filePath, password: password.value || undefined }),\n });\n if (!res.ok) {\n /* ⚠ Never the raw status. This printed \"503 Service Unavailable\" (or a\n * JSON body) straight into the preview — the same failure the owner\n * reported for ONLYOFFICE (\"Config fetch 503: {…}\"): a person is shown\n * the HTTP layer instead of what happened. The server's reason, when it\n * sent one we know, becomes a sentence; anything else is the generic\n * \"could not read the archive\". */\n const body = await res.json().catch(() => ({})) as { code?: string; error?: string };\n if (body.code === 'PASSWORD_REQUIRED' || body.code === 'BAD_PASSWORD') {\n props.archivePreviewCache?.forget(props.filePath);\n passwordNeeded.value = true;\n passwordError.value = body.code === 'BAD_PASSWORD'\n ? tt('archive.bad_password', 'The archive password is incorrect.')\n : '';\n return;\n }\n throw new ArchiveError(archiveErrorText(res.status, body.code ?? body.error ?? ''));\n }\n const body = (await res.json()) as { entries?: ArchiveEntry[] };\n entries.value = body.entries ?? [];\n if (password.value) props.archivePreviewCache?.remember(props.filePath, entries.value);\n passwordNeeded.value = false;\n } catch (err) {\n error.value =\n err instanceof ArchiveError ? err.message : tt('viewer.archive.error', 'Could not read archive contents.');\n } finally {\n loading.value = false;\n }\n}\n\n/** A failure already worded for a person. Anything else that throws (the\n * network, a JSON parse) reads as the generic sentence. */\nclass ArchiveError extends Error {}\n\nfunction archiveErrorText(status: number, code: string): string {\n if (/not a zip|unsupported|not supported|format/i.test(code)) {\n return tt('viewer.archive.unsupported', 'This kind of archive cannot be listed here. Download it to see what is inside.');\n }\n if (status === 413 || /too large|too big/i.test(code)) {\n return tt('viewer.archive.too_large', 'This archive is too large to list here. Download it to see what is inside.');\n }\n return tt('viewer.archive.error', 'Could not read archive contents.');\n}\n\n\n/**\n * The entries, in THE table (DataTable — the explorer's own). ⚠ It used to be\n * the one table in the package allowed to be its own, with the reasoning that\n * a file's contents answer to the viewer's layout. The owner's rule since\n * 2026-09-21 has no such exception — \"bir yere tablo gerekiyorsa bu tabloyu\n * koymak zorundayız\" — and an archive's listing is a file listing: it wants\n * to be sorted by size and have its Name column widened exactly the way the\n * folder it came from does.\n */\nconst columns = computed<DataColumn<ArchiveRow>[]>(() => [\n {\n id: 'name',\n label: tt('viewer.name', 'Name'),\n sortable: true,\n width: 360,\n class: 'filex-viewer-archive__name',\n title: (e) => e.name,\n },\n {\n id: 'size',\n label: tt('viewer.size', 'Size'),\n sortable: true,\n width: 110,\n align: 'right',\n class: 'filex-viewer-archive__size',\n format: (e) => (e.is_dir ? '' : fmtSize(e.size)),\n sortValue: (e) => (e.is_dir ? -1 : e.size),\n },\n]);\n\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nfunction submitPassword(value: string) {\n password.value = value;\n void load();\n}\n\nonMounted(load);\nwatch(() => props.filePath, () => {\n currentDir.value = '';\n password.value = '';\n passwordNeeded.value = false;\n void load();\n});\n\nconst totalSize = computed(() => entries.value.reduce((sum, entry) => sum + (entry.size || 0), 0));\nconst fileCount = computed(() => entries.value.filter((entry) => !entry.is_dir).length);\n\nfunction cleanPath(value: string): string {\n return value.replace(/\\\\/g, '/').replace(/^\\/+|\\/+$/g, '');\n}\n\nconst visibleEntries = computed<ArchiveRow[]>(() => {\n const rows = new Map<string, ArchiveRow>();\n for (const entry of entries.value) {\n const fullPath = cleanPath(entry.name);\n if (!fullPath || (currentDir.value && !fullPath.startsWith(currentDir.value))) continue;\n const rest = currentDir.value ? fullPath.slice(currentDir.value.length) : fullPath;\n if (!rest) continue;\n const slash = rest.indexOf('/');\n if (slash >= 0) {\n const displayName = rest.slice(0, slash);\n const dirPath = `${currentDir.value}${displayName}`;\n if (!rows.has(dirPath)) {\n rows.set(dirPath, { name: dirPath, fullPath: dirPath, displayName, size: 0, is_dir: true });\n }\n continue;\n }\n rows.set(fullPath, { ...entry, name: fullPath, fullPath, displayName: rest });\n }\n return [...rows.values()].sort((a, b) => {\n if (!!a.is_dir !== !!b.is_dir) return a.is_dir ? -1 : 1;\n return a.displayName.localeCompare(b.displayName, undefined, { numeric: true, sensitivity: 'base' });\n });\n});\n\nconst breadcrumbs = computed(() => {\n const parts = cleanPath(currentDir.value).split('/').filter(Boolean);\n return parts.map((name, index) => ({ name, path: `${parts.slice(0, index + 1).join('/')}/` }));\n});\n\nfunction openRow(row: ArchiveRow) {\n if (row.is_dir) currentDir.value = `${cleanPath(row.fullPath)}/`;\n\n}\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n\nfunction entryTile(entry: ArchiveEntry): string {\n const name = entry.name || '';\n const dot = name.lastIndexOf('.');\n return fileIconTile({\n type: entry.is_dir ? 'dir' : 'file',\n extension: dot > 0 ? name.slice(dot + 1) : '',\n basename: name,\n });\n}\n</script>\n\n<template>\n <div class=\"filex-viewer-archive\">\n <div v-if=\"passwordNeeded\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"actionIconSvg('lock')\"></span>\n <p>{{ tt('archive.password_required', 'This archive requires a password.') }}</p>\n </div>\n <div v-else-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <div v-else-if=\"loading\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span\n class=\"filex-viewer-fallback__icon filex-viewer-fallback__icon--spin\"\n aria-hidden=\"true\"\n v-html=\"actionIconSvg('progress')\"\n ></span>\n <p>{{ tt('viewer.loading', 'Loading…') }}</p>\n </div>\n <div v-else-if=\"entries.length === 0\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ tt('viewer.archive.empty', 'Archive is empty.') }}</p>\n </div>\n <div v-else class=\"filex-viewer-archive__pane\">\n <nav class=\"filex-viewer-archive__crumbs\" :aria-label=\"tt('viewer.archive.location', 'Archive location')\">\n <button type=\"button\" @click=\"currentDir = ''\">{{ tt('viewer.archive.root', 'Archive') }}</button>\n <template v-for=\"crumb in breadcrumbs\" :key=\"crumb.path\">\n <span aria-hidden=\"true\">›</span>\n <button type=\"button\" @click=\"currentDir = crumb.path\">{{ crumb.name }}</button>\n </template>\n </nav>\n <div class=\"filex-viewer-archive__summary\">\n <!-- ⚠ The count is IN the message (\"{n} files\"). A `{{ fileCount }}`\n printed in front of it read \"3 3 files\". -->\n {{ tt('viewer.archive.entries', `${fileCount} files`, { n: fileCount }) }}\n · {{ fmtSize(totalSize) }}\n </div>\n <DataTable\n table-id=\"viewer.archive\"\n :columns=\"columns\"\n :rows=\"visibleEntries\"\n :row-key=\"(e: ArchiveRow) => e.fullPath\"\n :row-attrs=\"(e: ArchiveRow) => ({ 'data-dir': e.is_dir ? '1' : '0', onDblclick: () => openRow(e) })\"\n >\n <template #cell-name=\"{ row }\">\n <!-- ikon:emoji — the same tile the listing draws for the same kind\n of file, so an archive's contents and the folder it came from\n read as one product. -->\n <button type=\"button\" class=\"filex-viewer-archive__entry\" :disabled=\"!row.is_dir\" @click=\"openRow(row)\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->\n <span class=\"filex-viewer-archive__icon\" aria-hidden=\"true\" v-html=\"entryTile(row)\"></span>\n <span class=\"fe-list__cell-text\">{{ row.displayName }}</span>\n </button>\n </template>\n </DataTable>\n </div>\n <ArchivePasswordModal\n :open=\"passwordNeeded\"\n :locale=\"props.locale\"\n :archive-name=\"(props.filePath || '').split('/').pop() || ''\"\n :busy=\"loading\"\n :error=\"passwordError\"\n @close=\"emit('close')\"\n @submit=\"submitPassword\"\n />\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-archive {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n}\n.filex-viewer-archive__pane {\n flex: 1;\n overflow: auto;\n padding: 16px 20px;\n}\n.filex-viewer-archive__summary {\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n margin-bottom: 12px;\n font-variant-numeric: tabular-nums;\n}\n/* The table itself is DataTable's (styles/base.css); only the viewer's own\n touches stay here — tabular figures and muted sizes, a folder row read as\n quieter than a file. */\n.filex-viewer-archive__size {\n text-align: end;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-archive__icon {\n margin-inline-end: 6px;\n}\n.filex-viewer-archive .fe-list__row[data-dir=\"1\"] {\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n.filex-viewer-archive__crumbs { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; color: var(--fe-text-muted, #5a6475); }\n.filex-viewer-archive__crumbs button { border: 0; background: transparent; color: inherit; cursor: pointer; padding: 2px 4px; border-radius: 4px; }\n.filex-viewer-archive__crumbs button:hover { background: var(--fe-bg-elev, #f7f8fa); color: var(--fe-text, #1a1e27); }\n.filex-viewer-archive__entry { display: inline-flex; align-items: center; max-width: 100%; border: 0; background: transparent; color: inherit; font: inherit; padding: 0; text-align: start; }\n.filex-viewer-archive__entry:not(:disabled) { cursor: pointer; }\n.filex-viewer-archive__entry:disabled { opacity: 1; }\n</style>\n"],"names":["props","__props","emit","__emit","entries","ref","currentDir","loading","error","password","passwordNeeded","passwordError","tt","key","fallback","vars","formatSize","useLocale","fmtSize","n","load","cached","_a","res","body","_b","ArchiveError","archiveErrorText","_c","err","status","code","columns","computed","submitPassword","value","onMounted","watch","totalSize","sum","entry","fileCount","cleanPath","visibleEntries","rows","fullPath","rest","slash","displayName","dirPath","b","breadcrumbs","parts","name","index","openRow","row","typeTile","fileIconTile","entryTile","dot","_openBlock","_createElementBlock","_hoisted_1","_hoisted_2","_createElementVNode","_unref","actionIconSvg","_hoisted_4","_hoisted_6","_hoisted_8","_hoisted_10","_Fragment","_renderList","crumb","_cache","$event","_toDisplayString","_hoisted_12","_hoisted_13","_createVNode","DataTable","e","_withCtx","_hoisted_16","ArchivePasswordModal"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent as q, ref as i, onMounted as A, watch as C, computed as c, openBlock as m, createElementBlock as w, createElementVNode as o, toDisplayString as _, unref as G, createBlock as Z, createTextVNode as z, createCommentVNode as J } from "vue";
|
|
2
|
-
import { f as K, a as Q, _ as U, b as X, s as Y } from "./index-
|
|
2
|
+
import { f as K, a as Q, _ as U, b as X, s as Y } from "./index-1RaeWGFO.js";
|
|
3
3
|
const ee = { class: "filex-viewer-csv" }, te = { class: "filex-viewer-csv__pane" }, ne = {
|
|
4
4
|
key: 0,
|
|
5
5
|
class: "filex-viewer-fallback"
|
|
@@ -169,4 +169,4 @@ const ee = { class: "filex-viewer-csv" }, te = { class: "filex-viewer-csv__pane"
|
|
|
169
169
|
export {
|
|
170
170
|
fe as default
|
|
171
171
|
};
|
|
172
|
-
//# sourceMappingURL=CsvViewer-
|
|
172
|
+
//# sourceMappingURL=CsvViewer-niuoz1Uk.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CsvViewer-B95fArq0.js","sources":["../src/viewers/CsvViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { sayFailure } from '../lib/errorWords';\n/**\n * CsvViewer — lightweight CSV / TSV table preview.\n *\n * Lazy-imports `papaparse` when available (best-quality parser — handles\n * embedded quotes, multi-line cells, etc). Falls back to a simple\n * `split` parser when the peer is missing — workable for the trivial\n * \"first 1000 rows\" preview case but loses fidelity on complex files.\n *\n * UX:\n * - first-row-as-header toggle\n * - filter input that case-insensitively matches any cell\n * - 100-rows-per-page pagination\n * - tab vs comma auto-detection (manual override via toolbar)\n */\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\nimport { fetchViewerText } from '../composables/useViewerFetch';\nimport DataTable, { type DataColumn } from '../components/DataTable.vue';\n\nconst props = defineProps<{\n url: string;\n ext: string;\n t?: (key: string, vars?: Record<string, string | number>) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n}>();\n\nconst PAGE_SIZE = 100;\nconst ROW_LIMIT = 1000;\n\nconst rows = ref<string[][]>([]);\nconst error = ref<string | null>(null);\nconst loading = ref(true);\nconst firstRowHeader = ref(true);\nconst filter = ref('');\nconst page = ref(1);\nconst detectedDelim = ref<',' | '\\t' | ';'>(',');\nconst userDelim = ref<'auto' | ',' | '\\t' | ';'>('auto');\n\nlet renderToken = 0;\n\nfunction detectDelimiter(sample: string): ',' | '\\t' | ';' {\n const sampleLines = sample.split(/\\r?\\n/).slice(0, 5).join('\\n');\n const tab = (sampleLines.match(/\\t/g) || []).length;\n const semi = (sampleLines.match(/;/g) || []).length;\n const comma = (sampleLines.match(/,/g) || []).length;\n if (tab > comma && tab > semi) return '\\t';\n if (semi > comma && semi > tab) return ';';\n return ',';\n}\n\nasync function parseWith(text: string, delim: string): Promise<string[][]> {\n try {\n const mod = await import(/* @vite-ignore */ 'papaparse');\n const Papa = (mod as any).default ?? mod;\n const result = Papa.parse(text, {\n delimiter: delim,\n skipEmptyLines: true,\n header: false,\n });\n return (result.data as string[][]).slice(0, ROW_LIMIT);\n } catch {\n // Fallback: naive line/column split. Loses quoted-comma support\n // but renders something reasonable.\n return text\n .split(/\\r?\\n/)\n .filter((l) => l.length > 0)\n .slice(0, ROW_LIMIT)\n .map((line) => line.split(delim));\n }\n}\n\nasync function load(): Promise<void> {\n loading.value = true;\n error.value = null;\n rows.value = [];\n page.value = 1;\n const myToken = ++renderToken;\n\n let text: string;\n try {\n text = await fetchViewerText({\n url: props.url,\n headers: (await props.authHeaders?.()) ?? {},\n credentials: props.authCredentials,\n });\n } catch (err) {\n error.value = sayFailure(err, tt('viewer.failed_to_load', 'Failed to load file'), { t: props.t }).text;\n loading.value = false;\n return;\n }\n\n if (myToken !== renderToken) return;\n\n if (props.ext === 'tsv') {\n detectedDelim.value = '\\t';\n } else {\n detectedDelim.value = detectDelimiter(text.slice(0, 4096));\n }\n\n const delim = userDelim.value === 'auto' ? detectedDelim.value : userDelim.value;\n rows.value = await parseWith(text, delim);\n loading.value = false;\n}\n\nonMounted(load);\nwatch(() => props.url, load);\nwatch(() => userDelim.value, () => {\n if (rows.value.length === 0) return;\n // Re-parse with the new delimiter using the cached source — but we\n // didn't keep it. Cheapest path is a refetch.\n load();\n});\n\nconst headers = computed<string[]>(() => {\n if (!firstRowHeader.value || rows.value.length === 0) {\n if (rows.value.length === 0) return [];\n return rows.value[0].map((_, i) => `Col ${i + 1}`);\n }\n return rows.value[0];\n});\n\nconst dataRows = computed<string[][]>(() => {\n return firstRowHeader.value ? rows.value.slice(1) : rows.value;\n});\n\n/** A parsed row with the line it came from, so `#` keeps naming the row in\n * the FILE after a sort re-orders the screen. */\ninterface CsvRow {\n n: number;\n cells: string[];\n}\n\nconst filtered = computed<CsvRow[]>(() => {\n const q = filter.value.trim().toLowerCase();\n const all = dataRows.value.map((cells, i) => ({ n: i + 1, cells }));\n if (!q) return all;\n return all.filter((r) => r.cells.some((c) => (c || '').toLowerCase().includes(q)));\n});\n\n/**\n * THE SORT, done here and handed to the table as CONTROLLED.\n *\n * ⚠⚠ Here and not in the table, because the table only ever holds one page\n * (100 rows) of the parsed file, and a table that sorts the page it holds\n * would call a re-ordered 100 of 1000 \"sorted\" — so it rightly closes its\n * headers over a paged list. This component holds EVERY parsed row, so it can\n * sort all of them and then page, which is the honest order.\n */\nconst sort = ref<{ key: string; dir: 'asc' | 'desc' } | null>(null);\n\nfunction cellOf(r: CsvRow, key: string): string {\n if (key === 'n') return String(r.n);\n return r.cells[Number(key.slice(1))] ?? '';\n}\n\nconst sorted = computed<CsvRow[]>(() => {\n const s = sort.value;\n if (!s) return filtered.value;\n const dir = s.dir === 'asc' ? 1 : -1;\n return [...filtered.value].sort((a, b) => {\n if (s.key === 'n') return dir * (a.n - b.n);\n const x = cellOf(a, s.key);\n const y = cellOf(b, s.key);\n // Empty cells last in both directions; numbers as numbers.\n if (!x !== !y) return x ? -1 : 1;\n const nx = Number(x);\n const ny = Number(y);\n if (x && y && Number.isFinite(nx) && Number.isFinite(ny)) return dir * (nx - ny) || a.n - b.n;\n return dir * x.localeCompare(y, undefined, { numeric: true, sensitivity: 'base' }) || a.n - b.n;\n });\n});\n\nfunction onSort(next: { key: string; dir: 'asc' | 'desc' }) {\n sort.value = next;\n}\n\nconst totalPages = computed(() =>\n Math.max(1, Math.ceil(sorted.value.length / PAGE_SIZE)),\n);\n\nconst visibleRows = computed<CsvRow[]>(() => {\n const start = (page.value - 1) * PAGE_SIZE;\n return sorted.value.slice(start, start + PAGE_SIZE);\n});\n\nwatch([filter, sort], () => {\n page.value = 1;\n});\n\n/** The file's columns, as THE table (DataTable — the explorer's own) draws\n * them. `#` is the lead: it is what names a row of a spreadsheet. */\nconst columns = computed<DataColumn<CsvRow>[]>(() => [\n {\n id: 'n',\n label: '#',\n lead: true,\n width: 64,\n min: 48,\n align: 'right',\n sortable: true,\n class: 'filex-viewer-csv__rownum',\n format: (r) => r.n,\n },\n ...headers.value.map((h, i) => ({\n id: `c${i}`,\n label: h,\n width: 140,\n sortable: true,\n format: (r: CsvRow) => r.cells[i] ?? '',\n })),\n]);\n\nfunction tt(key: string, fallback: string, vars?: Record<string, string | number>): string {\n return props.t ? props.t(key, vars) : fallback;\n}\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n</script>\n\n<template>\n <div class=\"filex-viewer-csv\">\n <div class=\"filex-viewer-csv__pane\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <div v-else-if=\"loading\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span\n class=\"filex-viewer-fallback__icon filex-viewer-fallback__icon--spin\"\n aria-hidden=\"true\"\n v-html=\"actionIconSvg('progress')\"\n ></span>\n <p>{{ tt('viewer.loading', 'Loading…') }}</p>\n </div>\n <!-- ⚠ No table id: a spreadsheet's columns are whatever THIS file has\n (`c0`, `c1`, …), so widths remembered against them would land on a\n different file's different columns next time. The arrangement\n lives for this preview. -->\n <DataTable\n v-else\n :table-id=\"undefined\"\n :columns=\"columns\"\n :rows=\"visibleRows\"\n :row-key=\"(r: CsvRow) => r.n\"\n :sort=\"sort\"\n @sort=\"onSort\"\n />\n </div>\n <div v-if=\"!loading && !error && totalPages > 1\" class=\"filex-viewer-csv__pager\">\n <button\n type=\"button\"\n class=\"filex-viewer-btn\"\n :disabled=\"page <= 1\"\n @click=\"page--\"\n >‹</button>\n <span class=\"filex-viewer-csv__pageno\"><bdi dir=\"ltr\">{{ page }} / {{ totalPages }}</bdi> ({{ tt('viewer.csv_rows', `${filtered.length} rows`, { n: filtered.length }) }})</span>\n <button\n type=\"button\"\n class=\"filex-viewer-btn\"\n :disabled=\"page >= totalPages\"\n @click=\"page++\"\n >›</button>\n </div>\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-csv {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n}\n.filex-viewer-csv__bar {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-bottom: 1px solid var(--fe-border, #e2e6ed);\n font-size: 12px;\n}\n.filex-viewer-csv__check {\n display: flex;\n align-items: center;\n gap: 4px;\n font-size: 12px;\n}\n.filex-viewer-csv__select,\n.filex-viewer-csv__filter {\n border: 1px solid var(--fe-border, #e2e6ed);\n background: var(--fe-bg, #fff);\n color: inherit;\n padding: 4px 8px;\n border-radius: 4px;\n font: inherit;\n font-size: 12px;\n}\n.filex-viewer-csv__filter {\n flex: 0 1 240px;\n}\n.filex-viewer-spacer { flex: 1; }\n.filex-viewer-csv__count {\n font-size: 11px;\n color: var(--fe-text-muted, #5a6475);\n font-variant-numeric: tabular-nums;\n}\n.filex-viewer-csv__pane {\n flex: 1;\n overflow: auto;\n}\n.filex-viewer-csv__pane .fe-list__cell {\n font-family: var(--fe-font-mono, monospace);\n font-size: 12px;\n}\n/* The table itself is DataTable's (styles/base.css) — the product's one table.\n ⚠ The row number used to pin itself (`position: sticky; left: 0`) because\n this was a table of its own; it is the table's LEAD now, and the table\n freezes its lead itself, only while there is room to (a pin written here\n would fight that gate). What stays is the look of a row number. */\n.filex-viewer-csv__rownum {\n color: var(--fe-text-muted, #5a6475);\n font-variant-numeric: tabular-nums;\n}\n.filex-viewer-csv__pager {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 8px;\n padding: 6px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-top: 1px solid var(--fe-border, #e2e6ed);\n font-size: 12px;\n}\n.filex-viewer-csv__pageno {\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n.filex-viewer-btn {\n border: 1px solid var(--fe-border, #e2e6ed);\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n}\n.filex-viewer-btn:hover:not(:disabled) {\n background: var(--fe-bg-hover, #edf0f5);\n}\n.filex-viewer-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n</style>\n"],"names":["PAGE_SIZE","ROW_LIMIT","props","__props","rows","ref","error","loading","firstRowHeader","filter","page","detectedDelim","userDelim","renderToken","detectDelimiter","sample","sampleLines","tab","semi","comma","parseWith","text","delim","mod","n","l","line","load","myToken","fetchViewerText","_a","err","sayFailure","tt","onMounted","watch","headers","computed","_","i","dataRows","filtered","q","all","cells","r","c","sort","cellOf","key","sorted","s","dir","a","b","x","y","nx","ny","onSort","next","totalPages","visibleRows","start","columns","h","fallback","vars","typeTile","fileIconTile","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_hoisted_5","_unref","actionIconSvg","_createBlock","DataTable","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_toDisplayString","_hoisted_11"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CsvViewer-niuoz1Uk.js","sources":["../src/viewers/CsvViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { sayFailure } from '../lib/errorWords';\n/**\n * CsvViewer — lightweight CSV / TSV table preview.\n *\n * Lazy-imports `papaparse` when available (best-quality parser — handles\n * embedded quotes, multi-line cells, etc). Falls back to a simple\n * `split` parser when the peer is missing — workable for the trivial\n * \"first 1000 rows\" preview case but loses fidelity on complex files.\n *\n * UX:\n * - first-row-as-header toggle\n * - filter input that case-insensitively matches any cell\n * - 100-rows-per-page pagination\n * - tab vs comma auto-detection (manual override via toolbar)\n */\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\nimport { fetchViewerText } from '../composables/useViewerFetch';\nimport DataTable, { type DataColumn } from '../components/DataTable.vue';\n\nconst props = defineProps<{\n url: string;\n ext: string;\n t?: (key: string, vars?: Record<string, string | number>) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n}>();\n\nconst PAGE_SIZE = 100;\nconst ROW_LIMIT = 1000;\n\nconst rows = ref<string[][]>([]);\nconst error = ref<string | null>(null);\nconst loading = ref(true);\nconst firstRowHeader = ref(true);\nconst filter = ref('');\nconst page = ref(1);\nconst detectedDelim = ref<',' | '\\t' | ';'>(',');\nconst userDelim = ref<'auto' | ',' | '\\t' | ';'>('auto');\n\nlet renderToken = 0;\n\nfunction detectDelimiter(sample: string): ',' | '\\t' | ';' {\n const sampleLines = sample.split(/\\r?\\n/).slice(0, 5).join('\\n');\n const tab = (sampleLines.match(/\\t/g) || []).length;\n const semi = (sampleLines.match(/;/g) || []).length;\n const comma = (sampleLines.match(/,/g) || []).length;\n if (tab > comma && tab > semi) return '\\t';\n if (semi > comma && semi > tab) return ';';\n return ',';\n}\n\nasync function parseWith(text: string, delim: string): Promise<string[][]> {\n try {\n const mod = await import(/* @vite-ignore */ 'papaparse');\n const Papa = (mod as any).default ?? mod;\n const result = Papa.parse(text, {\n delimiter: delim,\n skipEmptyLines: true,\n header: false,\n });\n return (result.data as string[][]).slice(0, ROW_LIMIT);\n } catch {\n // Fallback: naive line/column split. Loses quoted-comma support\n // but renders something reasonable.\n return text\n .split(/\\r?\\n/)\n .filter((l) => l.length > 0)\n .slice(0, ROW_LIMIT)\n .map((line) => line.split(delim));\n }\n}\n\nasync function load(): Promise<void> {\n loading.value = true;\n error.value = null;\n rows.value = [];\n page.value = 1;\n const myToken = ++renderToken;\n\n let text: string;\n try {\n text = await fetchViewerText({\n url: props.url,\n headers: (await props.authHeaders?.()) ?? {},\n credentials: props.authCredentials,\n });\n } catch (err) {\n error.value = sayFailure(err, tt('viewer.failed_to_load', 'Failed to load file'), { t: props.t }).text;\n loading.value = false;\n return;\n }\n\n if (myToken !== renderToken) return;\n\n if (props.ext === 'tsv') {\n detectedDelim.value = '\\t';\n } else {\n detectedDelim.value = detectDelimiter(text.slice(0, 4096));\n }\n\n const delim = userDelim.value === 'auto' ? detectedDelim.value : userDelim.value;\n rows.value = await parseWith(text, delim);\n loading.value = false;\n}\n\nonMounted(load);\nwatch(() => props.url, load);\nwatch(() => userDelim.value, () => {\n if (rows.value.length === 0) return;\n // Re-parse with the new delimiter using the cached source — but we\n // didn't keep it. Cheapest path is a refetch.\n load();\n});\n\nconst headers = computed<string[]>(() => {\n if (!firstRowHeader.value || rows.value.length === 0) {\n if (rows.value.length === 0) return [];\n return rows.value[0].map((_, i) => `Col ${i + 1}`);\n }\n return rows.value[0];\n});\n\nconst dataRows = computed<string[][]>(() => {\n return firstRowHeader.value ? rows.value.slice(1) : rows.value;\n});\n\n/** A parsed row with the line it came from, so `#` keeps naming the row in\n * the FILE after a sort re-orders the screen. */\ninterface CsvRow {\n n: number;\n cells: string[];\n}\n\nconst filtered = computed<CsvRow[]>(() => {\n const q = filter.value.trim().toLowerCase();\n const all = dataRows.value.map((cells, i) => ({ n: i + 1, cells }));\n if (!q) return all;\n return all.filter((r) => r.cells.some((c) => (c || '').toLowerCase().includes(q)));\n});\n\n/**\n * THE SORT, done here and handed to the table as CONTROLLED.\n *\n * ⚠⚠ Here and not in the table, because the table only ever holds one page\n * (100 rows) of the parsed file, and a table that sorts the page it holds\n * would call a re-ordered 100 of 1000 \"sorted\" — so it rightly closes its\n * headers over a paged list. This component holds EVERY parsed row, so it can\n * sort all of them and then page, which is the honest order.\n */\nconst sort = ref<{ key: string; dir: 'asc' | 'desc' } | null>(null);\n\nfunction cellOf(r: CsvRow, key: string): string {\n if (key === 'n') return String(r.n);\n return r.cells[Number(key.slice(1))] ?? '';\n}\n\nconst sorted = computed<CsvRow[]>(() => {\n const s = sort.value;\n if (!s) return filtered.value;\n const dir = s.dir === 'asc' ? 1 : -1;\n return [...filtered.value].sort((a, b) => {\n if (s.key === 'n') return dir * (a.n - b.n);\n const x = cellOf(a, s.key);\n const y = cellOf(b, s.key);\n // Empty cells last in both directions; numbers as numbers.\n if (!x !== !y) return x ? -1 : 1;\n const nx = Number(x);\n const ny = Number(y);\n if (x && y && Number.isFinite(nx) && Number.isFinite(ny)) return dir * (nx - ny) || a.n - b.n;\n return dir * x.localeCompare(y, undefined, { numeric: true, sensitivity: 'base' }) || a.n - b.n;\n });\n});\n\nfunction onSort(next: { key: string; dir: 'asc' | 'desc' }) {\n sort.value = next;\n}\n\nconst totalPages = computed(() =>\n Math.max(1, Math.ceil(sorted.value.length / PAGE_SIZE)),\n);\n\nconst visibleRows = computed<CsvRow[]>(() => {\n const start = (page.value - 1) * PAGE_SIZE;\n return sorted.value.slice(start, start + PAGE_SIZE);\n});\n\nwatch([filter, sort], () => {\n page.value = 1;\n});\n\n/** The file's columns, as THE table (DataTable — the explorer's own) draws\n * them. `#` is the lead: it is what names a row of a spreadsheet. */\nconst columns = computed<DataColumn<CsvRow>[]>(() => [\n {\n id: 'n',\n label: '#',\n lead: true,\n width: 64,\n min: 48,\n align: 'right',\n sortable: true,\n class: 'filex-viewer-csv__rownum',\n format: (r) => r.n,\n },\n ...headers.value.map((h, i) => ({\n id: `c${i}`,\n label: h,\n width: 140,\n sortable: true,\n format: (r: CsvRow) => r.cells[i] ?? '',\n })),\n]);\n\nfunction tt(key: string, fallback: string, vars?: Record<string, string | number>): string {\n return props.t ? props.t(key, vars) : fallback;\n}\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n</script>\n\n<template>\n <div class=\"filex-viewer-csv\">\n <div class=\"filex-viewer-csv__pane\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <div v-else-if=\"loading\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span\n class=\"filex-viewer-fallback__icon filex-viewer-fallback__icon--spin\"\n aria-hidden=\"true\"\n v-html=\"actionIconSvg('progress')\"\n ></span>\n <p>{{ tt('viewer.loading', 'Loading…') }}</p>\n </div>\n <!-- ⚠ No table id: a spreadsheet's columns are whatever THIS file has\n (`c0`, `c1`, …), so widths remembered against them would land on a\n different file's different columns next time. The arrangement\n lives for this preview. -->\n <DataTable\n v-else\n :table-id=\"undefined\"\n :columns=\"columns\"\n :rows=\"visibleRows\"\n :row-key=\"(r: CsvRow) => r.n\"\n :sort=\"sort\"\n @sort=\"onSort\"\n />\n </div>\n <div v-if=\"!loading && !error && totalPages > 1\" class=\"filex-viewer-csv__pager\">\n <button\n type=\"button\"\n class=\"filex-viewer-btn\"\n :disabled=\"page <= 1\"\n @click=\"page--\"\n >‹</button>\n <span class=\"filex-viewer-csv__pageno\"><bdi dir=\"ltr\">{{ page }} / {{ totalPages }}</bdi> ({{ tt('viewer.csv_rows', `${filtered.length} rows`, { n: filtered.length }) }})</span>\n <button\n type=\"button\"\n class=\"filex-viewer-btn\"\n :disabled=\"page >= totalPages\"\n @click=\"page++\"\n >›</button>\n </div>\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-csv {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n}\n.filex-viewer-csv__bar {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-bottom: 1px solid var(--fe-border, #e2e6ed);\n font-size: 12px;\n}\n.filex-viewer-csv__check {\n display: flex;\n align-items: center;\n gap: 4px;\n font-size: 12px;\n}\n.filex-viewer-csv__select,\n.filex-viewer-csv__filter {\n border: 1px solid var(--fe-border, #e2e6ed);\n background: var(--fe-bg, #fff);\n color: inherit;\n padding: 4px 8px;\n border-radius: 4px;\n font: inherit;\n font-size: 12px;\n}\n.filex-viewer-csv__filter {\n flex: 0 1 240px;\n}\n.filex-viewer-spacer { flex: 1; }\n.filex-viewer-csv__count {\n font-size: 11px;\n color: var(--fe-text-muted, #5a6475);\n font-variant-numeric: tabular-nums;\n}\n.filex-viewer-csv__pane {\n flex: 1;\n overflow: auto;\n}\n.filex-viewer-csv__pane .fe-list__cell {\n font-family: var(--fe-font-mono, monospace);\n font-size: 12px;\n}\n/* The table itself is DataTable's (styles/base.css) — the product's one table.\n ⚠ The row number used to pin itself (`position: sticky; left: 0`) because\n this was a table of its own; it is the table's LEAD now, and the table\n freezes its lead itself, only while there is room to (a pin written here\n would fight that gate). What stays is the look of a row number. */\n.filex-viewer-csv__rownum {\n color: var(--fe-text-muted, #5a6475);\n font-variant-numeric: tabular-nums;\n}\n.filex-viewer-csv__pager {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 8px;\n padding: 6px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-top: 1px solid var(--fe-border, #e2e6ed);\n font-size: 12px;\n}\n.filex-viewer-csv__pageno {\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n.filex-viewer-btn {\n border: 1px solid var(--fe-border, #e2e6ed);\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n}\n.filex-viewer-btn:hover:not(:disabled) {\n background: var(--fe-bg-hover, #edf0f5);\n}\n.filex-viewer-btn:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n</style>\n"],"names":["PAGE_SIZE","ROW_LIMIT","props","__props","rows","ref","error","loading","firstRowHeader","filter","page","detectedDelim","userDelim","renderToken","detectDelimiter","sample","sampleLines","tab","semi","comma","parseWith","text","delim","mod","n","l","line","load","myToken","fetchViewerText","_a","err","sayFailure","tt","onMounted","watch","headers","computed","_","i","dataRows","filtered","q","all","cells","r","c","sort","cellOf","key","sorted","s","dir","a","b","x","y","nx","ny","onSort","next","totalPages","visibleRows","start","columns","h","fallback","vars","typeTile","fileIconTile","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_hoisted_5","_unref","actionIconSvg","_createBlock","DataTable","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_toDisplayString","_hoisted_11"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent as U, ref as s, computed as c, onMounted as B, watch as C, onBeforeUnmount as E, openBlock as f, createElementBlock as v, createElementVNode as x, toDisplayString as O } from "vue";
|
|
2
|
-
import { f as D, b as H } from "./index-
|
|
2
|
+
import { f as D, b as H } from "./index-1RaeWGFO.js";
|
|
3
3
|
const L = { class: "filex-viewer-drawio" }, M = {
|
|
4
4
|
key: 0,
|
|
5
5
|
class: "filex-viewer-fallback"
|
|
@@ -144,4 +144,4 @@ const L = { class: "filex-viewer-drawio" }, M = {
|
|
|
144
144
|
export {
|
|
145
145
|
J as default
|
|
146
146
|
};
|
|
147
|
-
//# sourceMappingURL=DrawioViewer-
|
|
147
|
+
//# sourceMappingURL=DrawioViewer-BtqMiasr.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DrawioViewer-BAaPMTge.js","sources":["../src/viewers/DrawioViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * DrawioViewer — embed diagrams.net via iframe + postMessage handshake.\n *\n * The diagrams.net embed mode (`?embed=1&proto=json`) handshakes via\n * window.postMessage:\n *\n * 1. iframe sends {event:'init'} ← bootstrap\n * 2. parent sends {action:'load', xml} ← our payload\n * 3. iframe sends {event:'save', xml} on save ← we POST back\n *\n * No external library required — diagrams.net hosts the entire editor\n * inside the iframe. We just fetch the source XML on mount and route\n * `save` events back to the configured `pdfSaveUrl`-style endpoint\n * (drawio uses the same shape: POST `{path, content}` body).\n */\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\nimport { fetchViewerText } from '../composables/useViewerFetch';\n\nconst props = defineProps<{\n url: string;\n filePath: string;\n ext: string;\n drawioUrl?: string;\n /** Optional save endpoint. When unset the iframe stays read-only. */\n saveUrl?: string;\n readOnly?: boolean;\n t?: (key: string) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n /** Could this person set up draw.io (`capabilities.caller_admin`)? Picks\n * which sentence the \"not configured\" pane says — see `bootIfReady`. */\n canConfigure?: boolean;\n}>();\n\nconst iframeRef = ref<HTMLIFrameElement | null>(null);\nconst error = ref<string | null>(null);\nconst status = ref<'loading' | 'ready' | 'saving' | 'saved' | 'error'>('loading');\nconst readOnly = ref(!!props.readOnly || !props.saveUrl);\n\n// drawioUrl=null is the operator's \"off\" signal — FileExplorer wipes it\n// when the capabilities probe reports drawio offline. Don't silently fall\n// back to the public embed.diagrams.net iframe, that defeats the point of\n// gating: render a \"not configured\" pane instead so the user understands\n// why the editor isn't loading.\n//\n// `drawioBase` MUST be a computed (not a const), otherwise a stale null\n// at mount time (capabilities probe still inflight) gets cached for the\n// component's lifetime — leading to the spurious\n// \"viewer.drawio.disabled\" fallback after the capability flips to \"ok\".\nconst drawioBase = computed<string | null>(() =>\n props.drawioUrl ? props.drawioUrl.replace(/\\/$/, '') : null,\n);\nconst iframeSrc = computed<string>(() => {\n if (!drawioBase.value) return '';\n const params = new URLSearchParams({\n embed: '1',\n proto: 'json',\n spin: '1',\n saveAndExit: '0',\n noSaveBtn: readOnly.value ? '1' : '0',\n noExitBtn: '1',\n ui: 'kennedy',\n modified: 'unsavedChanges',\n });\n return `${drawioBase.value}/?${params.toString()}`;\n});\n\nlet pendingXml: string = '';\n\nasync function loadXml(): Promise<void> {\n status.value = 'loading';\n error.value = null;\n try {\n pendingXml = await fetchViewerText({\n url: props.url,\n headers: (await props.authHeaders?.()) ?? {},\n credentials: props.authCredentials,\n });\n } catch (err) {\n /* ⚠ A sentence, never `${status} ${statusText} — <body>` (which is what\n fetchViewerText's error carries). The detail goes to the console. */\n console.warn('[filex] diagram could not be read', err);\n error.value = tt('viewer.drawio.load_failed', 'This diagram could not be read. Download it to open it elsewhere.');\n disabledPane = false;\n status.value = 'error';\n }\n}\n\nfunction send(msg: unknown): void {\n iframeRef.value?.contentWindow?.postMessage(JSON.stringify(msg), '*');\n}\n\nasync function persist(xml: string): Promise<void> {\n if (!props.saveUrl) return;\n status.value = 'saving';\n try {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...((await props.authHeaders?.()) ?? {}),\n };\n const res = await fetch(props.saveUrl, {\n method: 'POST',\n headers,\n credentials: props.authCredentials || 'same-origin',\n body: JSON.stringify({ path: props.filePath, content: xml }),\n });\n if (!res.ok) {\n /* ⚠ Never the HTTP layer on screen (\"503 Service Unavailable\" is what\n this used to say when a save could not take its version snapshot). */\n console.warn('[filex] diagram save failed', res.status, res.statusText);\n throw new Error('save');\n }\n status.value = 'saved';\n setTimeout(() => {\n if (status.value === 'saved') status.value = 'ready';\n }, 2500);\n } catch (err) {\n console.warn('[filex] diagram save failed', err);\n error.value = tt('viewer.drawio.save_failed', 'The diagram could not be saved. Your changes are still in the editor — try again.');\n disabledPane = false;\n status.value = 'error';\n }\n}\n\nfunction onMessage(ev: MessageEvent): void {\n if (typeof ev.data !== 'string' || ev.data.length === 0) return;\n // Drawio messages always start with '{' or '<'. Ignore anything else\n // (devtools / unrelated postMessage senders inside the same window).\n const first = ev.data[0];\n if (first !== '{' && first !== '<') return;\n let payload: any;\n try {\n payload = JSON.parse(ev.data);\n } catch {\n return;\n }\n if (!payload || typeof payload !== 'object') return;\n switch (payload.event) {\n case 'init':\n send({\n action: 'load',\n xml: pendingXml,\n autosave: 0,\n });\n status.value = 'ready';\n break;\n case 'save':\n if (typeof payload.xml === 'string') {\n persist(payload.xml);\n }\n break;\n case 'export':\n // Could persist a PNG/SVG render; out of V1 scope.\n break;\n case 'autosave':\n if (typeof payload.xml === 'string' && !readOnly.value) {\n persist(payload.xml);\n }\n break;\n default:\n break;\n }\n}\n\n/** Is the pane on screen the \"not configured\" one (so a later answer that\n * draw.io IS there can clear it)? A flag, not a comparison with the text:\n * the text now depends on who is looking. */\nlet disabledPane = false;\n\nfunction bootIfReady(): void {\n if (!drawioBase.value) {\n /* The owner's rule for a missing service (2026-09-21): whoever can fix\n it is told where; everybody else is told what they can do instead.\n ⚠ The old sentence named an ENVIRONMENT VARIABLE to every reader —\n \"operator must wire FILEX_DRAWIO_URL\" — which is a riddle to a person\n who only wanted to look at a diagram. */\n error.value = props.canConfigure\n ? tt('viewer.drawio.disabled_admin', 'draw.io is not set up on this server. Set it up in the admin panel under External services.')\n : tt('viewer.drawio.disabled', 'Diagrams cannot be opened here: this server has no draw.io. Download the file to open it elsewhere.');\n disabledPane = true;\n status.value = 'error';\n return;\n }\n // Clear a previous \"disabled\" pane in case the prop just became available\n // (capability probe finished after mount).\n if (disabledPane) {\n disabledPane = false;\n error.value = null;\n status.value = 'loading';\n }\n loadXml();\n}\n\nonMounted(() => {\n window.addEventListener('message', onMessage);\n bootIfReady();\n});\n\nwatch(() => props.drawioUrl, bootIfReady);\n\nonBeforeUnmount(() => {\n window.removeEventListener('message', onMessage);\n});\n\nfunction tt(key: string, fallback: string): string {\n return props.t ? props.t(key) : fallback;\n}\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n</script>\n\n<template>\n <div class=\"filex-viewer-drawio\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <iframe\n v-else\n ref=\"iframeRef\"\n :src=\"iframeSrc\"\n class=\"filex-viewer-drawio__frame\"\n title=\"draw.io\"\n />\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-drawio {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n}\n.filex-viewer-drawio__bar {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-bottom: 1px solid var(--fe-border, #e2e6ed);\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-drawio__status[data-state=\"error\"] { color: var(--fe-danger, #dc2626); }\n.filex-viewer-drawio__status[data-state=\"saved\"] { color: #059669; }\n.filex-viewer-drawio__readonly {\n font-style: italic;\n margin-inline-start: auto;\n}\n.filex-viewer-drawio__frame {\n flex: 1;\n width: 100%;\n border: 0;\n background: #fafafa;\n min-height: 0;\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n</style>\n"],"names":["props","__props","iframeRef","ref","error","status","readOnly","drawioBase","computed","iframeSrc","params","pendingXml","loadXml","fetchViewerText","_a","err","tt","disabledPane","send","msg","_b","persist","xml","headers","res","onMessage","ev","first","payload","bootIfReady","onMounted","watch","onBeforeUnmount","key","fallback","typeTile","fileIconTile","_openBlock","_createElementBlock","_hoisted_1","_hoisted_2","_createElementVNode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DrawioViewer-BtqMiasr.js","sources":["../src/viewers/DrawioViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * DrawioViewer — embed diagrams.net via iframe + postMessage handshake.\n *\n * The diagrams.net embed mode (`?embed=1&proto=json`) handshakes via\n * window.postMessage:\n *\n * 1. iframe sends {event:'init'} ← bootstrap\n * 2. parent sends {action:'load', xml} ← our payload\n * 3. iframe sends {event:'save', xml} on save ← we POST back\n *\n * No external library required — diagrams.net hosts the entire editor\n * inside the iframe. We just fetch the source XML on mount and route\n * `save` events back to the configured `pdfSaveUrl`-style endpoint\n * (drawio uses the same shape: POST `{path, content}` body).\n */\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\nimport { fetchViewerText } from '../composables/useViewerFetch';\n\nconst props = defineProps<{\n url: string;\n filePath: string;\n ext: string;\n drawioUrl?: string;\n /** Optional save endpoint. When unset the iframe stays read-only. */\n saveUrl?: string;\n readOnly?: boolean;\n t?: (key: string) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n /** Could this person set up draw.io (`capabilities.caller_admin`)? Picks\n * which sentence the \"not configured\" pane says — see `bootIfReady`. */\n canConfigure?: boolean;\n}>();\n\nconst iframeRef = ref<HTMLIFrameElement | null>(null);\nconst error = ref<string | null>(null);\nconst status = ref<'loading' | 'ready' | 'saving' | 'saved' | 'error'>('loading');\nconst readOnly = ref(!!props.readOnly || !props.saveUrl);\n\n// drawioUrl=null is the operator's \"off\" signal — FileExplorer wipes it\n// when the capabilities probe reports drawio offline. Don't silently fall\n// back to the public embed.diagrams.net iframe, that defeats the point of\n// gating: render a \"not configured\" pane instead so the user understands\n// why the editor isn't loading.\n//\n// `drawioBase` MUST be a computed (not a const), otherwise a stale null\n// at mount time (capabilities probe still inflight) gets cached for the\n// component's lifetime — leading to the spurious\n// \"viewer.drawio.disabled\" fallback after the capability flips to \"ok\".\nconst drawioBase = computed<string | null>(() =>\n props.drawioUrl ? props.drawioUrl.replace(/\\/$/, '') : null,\n);\nconst iframeSrc = computed<string>(() => {\n if (!drawioBase.value) return '';\n const params = new URLSearchParams({\n embed: '1',\n proto: 'json',\n spin: '1',\n saveAndExit: '0',\n noSaveBtn: readOnly.value ? '1' : '0',\n noExitBtn: '1',\n ui: 'kennedy',\n modified: 'unsavedChanges',\n });\n return `${drawioBase.value}/?${params.toString()}`;\n});\n\nlet pendingXml: string = '';\n\nasync function loadXml(): Promise<void> {\n status.value = 'loading';\n error.value = null;\n try {\n pendingXml = await fetchViewerText({\n url: props.url,\n headers: (await props.authHeaders?.()) ?? {},\n credentials: props.authCredentials,\n });\n } catch (err) {\n /* ⚠ A sentence, never `${status} ${statusText} — <body>` (which is what\n fetchViewerText's error carries). The detail goes to the console. */\n console.warn('[filex] diagram could not be read', err);\n error.value = tt('viewer.drawio.load_failed', 'This diagram could not be read. Download it to open it elsewhere.');\n disabledPane = false;\n status.value = 'error';\n }\n}\n\nfunction send(msg: unknown): void {\n iframeRef.value?.contentWindow?.postMessage(JSON.stringify(msg), '*');\n}\n\nasync function persist(xml: string): Promise<void> {\n if (!props.saveUrl) return;\n status.value = 'saving';\n try {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...((await props.authHeaders?.()) ?? {}),\n };\n const res = await fetch(props.saveUrl, {\n method: 'POST',\n headers,\n credentials: props.authCredentials || 'same-origin',\n body: JSON.stringify({ path: props.filePath, content: xml }),\n });\n if (!res.ok) {\n /* ⚠ Never the HTTP layer on screen (\"503 Service Unavailable\" is what\n this used to say when a save could not take its version snapshot). */\n console.warn('[filex] diagram save failed', res.status, res.statusText);\n throw new Error('save');\n }\n status.value = 'saved';\n setTimeout(() => {\n if (status.value === 'saved') status.value = 'ready';\n }, 2500);\n } catch (err) {\n console.warn('[filex] diagram save failed', err);\n error.value = tt('viewer.drawio.save_failed', 'The diagram could not be saved. Your changes are still in the editor — try again.');\n disabledPane = false;\n status.value = 'error';\n }\n}\n\nfunction onMessage(ev: MessageEvent): void {\n if (typeof ev.data !== 'string' || ev.data.length === 0) return;\n // Drawio messages always start with '{' or '<'. Ignore anything else\n // (devtools / unrelated postMessage senders inside the same window).\n const first = ev.data[0];\n if (first !== '{' && first !== '<') return;\n let payload: any;\n try {\n payload = JSON.parse(ev.data);\n } catch {\n return;\n }\n if (!payload || typeof payload !== 'object') return;\n switch (payload.event) {\n case 'init':\n send({\n action: 'load',\n xml: pendingXml,\n autosave: 0,\n });\n status.value = 'ready';\n break;\n case 'save':\n if (typeof payload.xml === 'string') {\n persist(payload.xml);\n }\n break;\n case 'export':\n // Could persist a PNG/SVG render; out of V1 scope.\n break;\n case 'autosave':\n if (typeof payload.xml === 'string' && !readOnly.value) {\n persist(payload.xml);\n }\n break;\n default:\n break;\n }\n}\n\n/** Is the pane on screen the \"not configured\" one (so a later answer that\n * draw.io IS there can clear it)? A flag, not a comparison with the text:\n * the text now depends on who is looking. */\nlet disabledPane = false;\n\nfunction bootIfReady(): void {\n if (!drawioBase.value) {\n /* The owner's rule for a missing service (2026-09-21): whoever can fix\n it is told where; everybody else is told what they can do instead.\n ⚠ The old sentence named an ENVIRONMENT VARIABLE to every reader —\n \"operator must wire FILEX_DRAWIO_URL\" — which is a riddle to a person\n who only wanted to look at a diagram. */\n error.value = props.canConfigure\n ? tt('viewer.drawio.disabled_admin', 'draw.io is not set up on this server. Set it up in the admin panel under External services.')\n : tt('viewer.drawio.disabled', 'Diagrams cannot be opened here: this server has no draw.io. Download the file to open it elsewhere.');\n disabledPane = true;\n status.value = 'error';\n return;\n }\n // Clear a previous \"disabled\" pane in case the prop just became available\n // (capability probe finished after mount).\n if (disabledPane) {\n disabledPane = false;\n error.value = null;\n status.value = 'loading';\n }\n loadXml();\n}\n\nonMounted(() => {\n window.addEventListener('message', onMessage);\n bootIfReady();\n});\n\nwatch(() => props.drawioUrl, bootIfReady);\n\nonBeforeUnmount(() => {\n window.removeEventListener('message', onMessage);\n});\n\nfunction tt(key: string, fallback: string): string {\n return props.t ? props.t(key) : fallback;\n}\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n</script>\n\n<template>\n <div class=\"filex-viewer-drawio\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <iframe\n v-else\n ref=\"iframeRef\"\n :src=\"iframeSrc\"\n class=\"filex-viewer-drawio__frame\"\n title=\"draw.io\"\n />\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-drawio {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n}\n.filex-viewer-drawio__bar {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-bottom: 1px solid var(--fe-border, #e2e6ed);\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-drawio__status[data-state=\"error\"] { color: var(--fe-danger, #dc2626); }\n.filex-viewer-drawio__status[data-state=\"saved\"] { color: #059669; }\n.filex-viewer-drawio__readonly {\n font-style: italic;\n margin-inline-start: auto;\n}\n.filex-viewer-drawio__frame {\n flex: 1;\n width: 100%;\n border: 0;\n background: #fafafa;\n min-height: 0;\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n</style>\n"],"names":["props","__props","iframeRef","ref","error","status","readOnly","drawioBase","computed","iframeSrc","params","pendingXml","loadXml","fetchViewerText","_a","err","tt","disabledPane","send","msg","_b","persist","xml","headers","res","onMessage","ev","first","payload","bootIfReady","onMounted","watch","onBeforeUnmount","key","fallback","typeTile","fileIconTile","_openBlock","_createElementBlock","_hoisted_1","_hoisted_2","_createElementVNode"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent as F, ref as o, onMounted as H, onBeforeUnmount as M, watch as S, computed as D, openBlock as u, createElementBlock as c, createElementVNode as d, toDisplayString as b, Fragment as R, createCommentVNode as E } from "vue";
|
|
2
|
-
import { f as V, r as z } from "./index-
|
|
2
|
+
import { f as V, r as z } from "./index-1RaeWGFO.js";
|
|
3
3
|
const A = {
|
|
4
4
|
key: 0,
|
|
5
5
|
class: "filex-viewer-fallback"
|
|
@@ -160,4 +160,4 @@ const A = {
|
|
|
160
160
|
export {
|
|
161
161
|
G as default
|
|
162
162
|
};
|
|
163
|
-
//# sourceMappingURL=EpubViewer-
|
|
163
|
+
//# sourceMappingURL=EpubViewer-ERm7L1WN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EpubViewer-B-B7bRW7.js","sources":["../src/viewers/EpubViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { requestFailure } from '../lib/errorWords';\n/**\n * EpubViewer — flowable EPUB reader via `epubjs`.\n *\n * Lazy-imports `epubjs` (~80 KB gzipped). epub.js renders the publication\n * inside an iframe (default `flow: 'paginated'`), but we use it in\n * scrolled-doc mode only when the user toggles the \"fit-width\" flag — by\n * default we keep paginated since that matches reader-app expectations.\n *\n * UX:\n * - prev/next page buttons (also keyboard arrow keys via composable)\n * - collapsible TOC sidebar (built from `book.loaded.navigation`)\n * - font size +/- (Themes API)\n * - graceful fallback if peer not installed\n */\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\n\nconst props = defineProps<{\n url: string;\n mime?: string;\n ext: string;\n t?: (key: string) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n}>();\n\ninterface TocNode {\n href: string;\n label: string;\n subitems?: TocNode[];\n}\n\nconst root = ref<HTMLDivElement | null>(null);\nconst containerRef = ref<HTMLDivElement | null>(null);\nconst error = ref<string | null>(null);\nconst toc = ref<TocNode[]>([]);\nconst tocOpen = ref(false);\nconst fontSize = ref(100);\nconst ready = ref(false);\nconst loading = ref(true);\n\nlet book: any = null;\nlet rendition: any = null;\n\n/**\n * How long an epub.js step may hang before we call it a failure.\n *\n * Generous on purpose: a large publication on a slow disk legitimately takes\n * seconds, and a false \"cannot open\" on a book that would have rendered is a\n * worse bug than the one this guards against.\n */\nconst EPUB_STEP_TIMEOUT_MS = 15000;\n\n/** Reject if `p` has not settled within `EPUB_STEP_TIMEOUT_MS`. */\nfunction withTimeout<T>(p: Promise<T>, label: string): Promise<T> {\n let timer: ReturnType<typeof setTimeout>;\n return Promise.race([\n p,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error(`EPUB ${label} timed out after ${EPUB_STEP_TIMEOUT_MS}ms`)),\n EPUB_STEP_TIMEOUT_MS,\n );\n }),\n ]).finally(() => clearTimeout(timer)) as Promise<T>;\n}\n\nasync function load(): Promise<void> {\n loading.value = true;\n ready.value = false;\n error.value = null;\n let mod: any = null;\n try {\n mod = await import(/* @vite-ignore */ 'epubjs');\n } catch {\n error.value = tt('viewer.peer_not_installed', 'This kind of file cannot be shown here. Download it to open it on your device.');\n loading.value = false;\n return;\n }\n try {\n const Epub = mod.default ?? mod;\n // epub.js wants a URL (it then fetches with XHR). When the host\n // site requires auth headers we have to load the file ourselves\n // and pass an ArrayBuffer instead.\n let source: string | ArrayBuffer = props.url;\n if (props.authHeaders) {\n const headers = await props.authHeaders();\n const res = await fetch(props.url, {\n headers,\n credentials: props.authCredentials || 'same-origin',\n });\n if (!res.ok) throw requestFailure(res.status, await res.text().catch(() => ''), undefined);\n source = await res.arrayBuffer();\n }\n book = Epub(source);\n if (!containerRef.value) {\n throw new Error('EPUB mount target missing');\n }\n rendition = book.renderTo(containerRef.value, {\n width: '100%',\n height: '100%',\n flow: 'paginated',\n manager: 'default',\n });\n // ⚠⚠ Both of these have to be raced against a clock. epub.js resolves\n // `display()` and `loaded.navigation` from its own internal promises, and\n // when the archive is not a readable EPUB it simply NEVER settles —\n // neither resolve nor reject — so the `catch` below can't fire and\n // `finally` never runs. Measured 2026-09-13 on an 8-byte `handbook.epub`:\n // the pane sat on \"Yükleniyor…\" forever with no error anywhere.\n await withTimeout(rendition.display(), 'display');\n rendition.themes.fontSize(fontSize.value + '%');\n // `<any>` explicitly: `book` is `any`, so without it `T` infers as `{}`\n // and `nav.toc` stops type-checking.\n const nav = await withTimeout<any>(book.loaded.navigation, 'navigation');\n toc.value = (nav?.toc ?? []) as TocNode[];\n ready.value = true;\n } catch (err) {\n // The reader gets the localized sentence; epub.js's own wording (and the\n // timeout marker) goes to the console for whoever is debugging.\n console.warn('[filex] EPUB load failed:', err);\n error.value = tt('viewer.failed_to_load', 'Failed to load file');\n } finally {\n loading.value = false;\n }\n}\n\nfunction next(): void {\n rendition?.next?.();\n}\nfunction prev(): void {\n rendition?.prev?.();\n}\nfunction gotoHref(href: string): void {\n rendition?.display?.(href);\n tocOpen.value = false;\n}\nfunction bigger(): void {\n fontSize.value = Math.min(200, fontSize.value + 10);\n rendition?.themes?.fontSize(fontSize.value + '%');\n}\nfunction smaller(): void {\n fontSize.value = Math.max(60, fontSize.value - 10);\n rendition?.themes?.fontSize(fontSize.value + '%');\n}\n\nfunction onKey(ev: KeyboardEvent): void {\n if (ev.key === 'ArrowRight' || ev.key === 'PageDown') next();\n else if (ev.key === 'ArrowLeft' || ev.key === 'PageUp') prev();\n}\n\nonMounted(() => {\n load();\n window.addEventListener('keydown', onKey);\n});\n\nonBeforeUnmount(() => {\n window.removeEventListener('keydown', onKey);\n try {\n rendition?.destroy?.();\n } catch {\n /* ignore */\n }\n try {\n book?.destroy?.();\n } catch {\n /* ignore */\n }\n rendition = null;\n book = null;\n});\n\nwatch(\n () => props.url,\n () => {\n if (rendition) {\n try {\n rendition.destroy();\n } catch {\n /* ignore */\n }\n }\n if (book) {\n try {\n book.destroy();\n } catch {\n /* ignore */\n }\n }\n book = null;\n rendition = null;\n load();\n },\n);\n\nfunction tt(key: string, fallback: string): string {\n return props.t ? props.t(key) : fallback;\n}\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n</script>\n\n<template>\n <div ref=\"root\" class=\"filex-viewer-epub\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <template v-else>\n <div ref=\"containerRef\" class=\"filex-viewer-epub__rendition\" />\n <div v-if=\"ready\" class=\"filex-viewer-epub__nav\">\n <button type=\"button\" class=\"filex-viewer-btn\" @click=\"prev\" :disabled=\"!ready\">‹</button>\n <button type=\"button\" class=\"filex-viewer-btn\" @click=\"next\" :disabled=\"!ready\">›</button>\n </div>\n <div v-if=\"loading\" class=\"filex-viewer-epub__loading\">\n {{ tt('viewer.loading', 'Loading…') }}\n </div>\n </template>\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-epub__nav {\n position: absolute;\n bottom: 12px;\n left: 50%; /* rtl-physical: centred by translate(-50%) — the same place in both directions */\n transform: translateX(-50%);\n display: flex;\n gap: 8px;\n padding: 4px 8px;\n background: var(--fe-bg-elev, rgba(255, 255, 255, 0.9));\n border: 1px solid var(--fe-border, #e2e6ed);\n border-radius: 6px;\n backdrop-filter: blur(4px);\n}\n.filex-viewer-epub {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n position: relative;\n}\n.filex-viewer-epub__bar {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-bottom: 1px solid var(--fe-border, #e2e6ed);\n font-size: 13px;\n}\n.filex-viewer-spacer { flex: 1; }\n.filex-viewer-epub__fs {\n display: inline-block;\n min-width: 40px;\n text-align: center;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-epub__main {\n flex: 1;\n min-height: 0;\n display: flex;\n}\n.filex-viewer-epub__rendition {\n flex: 1;\n min-height: 0;\n}\n.filex-viewer-epub__toc {\n width: 260px;\n border-inline-end: 1px solid var(--fe-border, #e2e6ed);\n overflow-y: auto;\n padding: 12px 0;\n background: var(--fe-bg-elev, #f7f8fa);\n}\n.filex-viewer-epub__toc ul {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n.filex-viewer-epub__toc-link {\n display: block;\n width: 100%;\n text-align: start;\n background: transparent;\n border: 0;\n padding: 6px 14px;\n font: inherit;\n color: inherit;\n cursor: pointer;\n}\n.filex-viewer-epub__toc-link:hover {\n background: var(--fe-bg-hover, #edf0f5);\n}\n.filex-viewer-epub__toc-link.is-child {\n padding-inline-start: 28px;\n color: var(--fe-text-muted, #5a6475);\n font-size: 12px;\n}\n.filex-viewer-epub__loading {\n position: absolute;\n inset: auto 0 0 0;\n text-align: center;\n padding: 6px;\n background: rgba(0, 0, 0, 0.05);\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n.filex-viewer-btn {\n border: 1px solid var(--fe-border, #e2e6ed);\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n}\n.filex-viewer-btn:hover:not(:disabled) {\n background: var(--fe-bg-hover, #edf0f5);\n}\n.filex-viewer-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n</style>\n"],"names":["EPUB_STEP_TIMEOUT_MS","props","__props","root","ref","containerRef","error","toc","fontSize","ready","loading","book","rendition","withTimeout","p","label","timer","_","reject","load","mod","tt","Epub","source","headers","res","requestFailure","nav","err","next","_a","prev","onKey","ev","onMounted","onBeforeUnmount","_b","watch","key","fallback","typeTile","computed","fileIconTile","_createElementBlock","_openBlock","_hoisted_1","_createElementVNode","_Fragment","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_toDisplayString"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EpubViewer-ERm7L1WN.js","sources":["../src/viewers/EpubViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { requestFailure } from '../lib/errorWords';\n/**\n * EpubViewer — flowable EPUB reader via `epubjs`.\n *\n * Lazy-imports `epubjs` (~80 KB gzipped). epub.js renders the publication\n * inside an iframe (default `flow: 'paginated'`), but we use it in\n * scrolled-doc mode only when the user toggles the \"fit-width\" flag — by\n * default we keep paginated since that matches reader-app expectations.\n *\n * UX:\n * - prev/next page buttons (also keyboard arrow keys via composable)\n * - collapsible TOC sidebar (built from `book.loaded.navigation`)\n * - font size +/- (Themes API)\n * - graceful fallback if peer not installed\n */\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\n\nconst props = defineProps<{\n url: string;\n mime?: string;\n ext: string;\n t?: (key: string) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n}>();\n\ninterface TocNode {\n href: string;\n label: string;\n subitems?: TocNode[];\n}\n\nconst root = ref<HTMLDivElement | null>(null);\nconst containerRef = ref<HTMLDivElement | null>(null);\nconst error = ref<string | null>(null);\nconst toc = ref<TocNode[]>([]);\nconst tocOpen = ref(false);\nconst fontSize = ref(100);\nconst ready = ref(false);\nconst loading = ref(true);\n\nlet book: any = null;\nlet rendition: any = null;\n\n/**\n * How long an epub.js step may hang before we call it a failure.\n *\n * Generous on purpose: a large publication on a slow disk legitimately takes\n * seconds, and a false \"cannot open\" on a book that would have rendered is a\n * worse bug than the one this guards against.\n */\nconst EPUB_STEP_TIMEOUT_MS = 15000;\n\n/** Reject if `p` has not settled within `EPUB_STEP_TIMEOUT_MS`. */\nfunction withTimeout<T>(p: Promise<T>, label: string): Promise<T> {\n let timer: ReturnType<typeof setTimeout>;\n return Promise.race([\n p,\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error(`EPUB ${label} timed out after ${EPUB_STEP_TIMEOUT_MS}ms`)),\n EPUB_STEP_TIMEOUT_MS,\n );\n }),\n ]).finally(() => clearTimeout(timer)) as Promise<T>;\n}\n\nasync function load(): Promise<void> {\n loading.value = true;\n ready.value = false;\n error.value = null;\n let mod: any = null;\n try {\n mod = await import(/* @vite-ignore */ 'epubjs');\n } catch {\n error.value = tt('viewer.peer_not_installed', 'This kind of file cannot be shown here. Download it to open it on your device.');\n loading.value = false;\n return;\n }\n try {\n const Epub = mod.default ?? mod;\n // epub.js wants a URL (it then fetches with XHR). When the host\n // site requires auth headers we have to load the file ourselves\n // and pass an ArrayBuffer instead.\n let source: string | ArrayBuffer = props.url;\n if (props.authHeaders) {\n const headers = await props.authHeaders();\n const res = await fetch(props.url, {\n headers,\n credentials: props.authCredentials || 'same-origin',\n });\n if (!res.ok) throw requestFailure(res.status, await res.text().catch(() => ''), undefined);\n source = await res.arrayBuffer();\n }\n book = Epub(source);\n if (!containerRef.value) {\n throw new Error('EPUB mount target missing');\n }\n rendition = book.renderTo(containerRef.value, {\n width: '100%',\n height: '100%',\n flow: 'paginated',\n manager: 'default',\n });\n // ⚠⚠ Both of these have to be raced against a clock. epub.js resolves\n // `display()` and `loaded.navigation` from its own internal promises, and\n // when the archive is not a readable EPUB it simply NEVER settles —\n // neither resolve nor reject — so the `catch` below can't fire and\n // `finally` never runs. Measured 2026-09-13 on an 8-byte `handbook.epub`:\n // the pane sat on \"Yükleniyor…\" forever with no error anywhere.\n await withTimeout(rendition.display(), 'display');\n rendition.themes.fontSize(fontSize.value + '%');\n // `<any>` explicitly: `book` is `any`, so without it `T` infers as `{}`\n // and `nav.toc` stops type-checking.\n const nav = await withTimeout<any>(book.loaded.navigation, 'navigation');\n toc.value = (nav?.toc ?? []) as TocNode[];\n ready.value = true;\n } catch (err) {\n // The reader gets the localized sentence; epub.js's own wording (and the\n // timeout marker) goes to the console for whoever is debugging.\n console.warn('[filex] EPUB load failed:', err);\n error.value = tt('viewer.failed_to_load', 'Failed to load file');\n } finally {\n loading.value = false;\n }\n}\n\nfunction next(): void {\n rendition?.next?.();\n}\nfunction prev(): void {\n rendition?.prev?.();\n}\nfunction gotoHref(href: string): void {\n rendition?.display?.(href);\n tocOpen.value = false;\n}\nfunction bigger(): void {\n fontSize.value = Math.min(200, fontSize.value + 10);\n rendition?.themes?.fontSize(fontSize.value + '%');\n}\nfunction smaller(): void {\n fontSize.value = Math.max(60, fontSize.value - 10);\n rendition?.themes?.fontSize(fontSize.value + '%');\n}\n\nfunction onKey(ev: KeyboardEvent): void {\n if (ev.key === 'ArrowRight' || ev.key === 'PageDown') next();\n else if (ev.key === 'ArrowLeft' || ev.key === 'PageUp') prev();\n}\n\nonMounted(() => {\n load();\n window.addEventListener('keydown', onKey);\n});\n\nonBeforeUnmount(() => {\n window.removeEventListener('keydown', onKey);\n try {\n rendition?.destroy?.();\n } catch {\n /* ignore */\n }\n try {\n book?.destroy?.();\n } catch {\n /* ignore */\n }\n rendition = null;\n book = null;\n});\n\nwatch(\n () => props.url,\n () => {\n if (rendition) {\n try {\n rendition.destroy();\n } catch {\n /* ignore */\n }\n }\n if (book) {\n try {\n book.destroy();\n } catch {\n /* ignore */\n }\n }\n book = null;\n rendition = null;\n load();\n },\n);\n\nfunction tt(key: string, fallback: string): string {\n return props.t ? props.t(key) : fallback;\n}\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n</script>\n\n<template>\n <div ref=\"root\" class=\"filex-viewer-epub\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <template v-else>\n <div ref=\"containerRef\" class=\"filex-viewer-epub__rendition\" />\n <div v-if=\"ready\" class=\"filex-viewer-epub__nav\">\n <button type=\"button\" class=\"filex-viewer-btn\" @click=\"prev\" :disabled=\"!ready\">‹</button>\n <button type=\"button\" class=\"filex-viewer-btn\" @click=\"next\" :disabled=\"!ready\">›</button>\n </div>\n <div v-if=\"loading\" class=\"filex-viewer-epub__loading\">\n {{ tt('viewer.loading', 'Loading…') }}\n </div>\n </template>\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-epub__nav {\n position: absolute;\n bottom: 12px;\n left: 50%; /* rtl-physical: centred by translate(-50%) — the same place in both directions */\n transform: translateX(-50%);\n display: flex;\n gap: 8px;\n padding: 4px 8px;\n background: var(--fe-bg-elev, rgba(255, 255, 255, 0.9));\n border: 1px solid var(--fe-border, #e2e6ed);\n border-radius: 6px;\n backdrop-filter: blur(4px);\n}\n.filex-viewer-epub {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n position: relative;\n}\n.filex-viewer-epub__bar {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-bottom: 1px solid var(--fe-border, #e2e6ed);\n font-size: 13px;\n}\n.filex-viewer-spacer { flex: 1; }\n.filex-viewer-epub__fs {\n display: inline-block;\n min-width: 40px;\n text-align: center;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-epub__main {\n flex: 1;\n min-height: 0;\n display: flex;\n}\n.filex-viewer-epub__rendition {\n flex: 1;\n min-height: 0;\n}\n.filex-viewer-epub__toc {\n width: 260px;\n border-inline-end: 1px solid var(--fe-border, #e2e6ed);\n overflow-y: auto;\n padding: 12px 0;\n background: var(--fe-bg-elev, #f7f8fa);\n}\n.filex-viewer-epub__toc ul {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n.filex-viewer-epub__toc-link {\n display: block;\n width: 100%;\n text-align: start;\n background: transparent;\n border: 0;\n padding: 6px 14px;\n font: inherit;\n color: inherit;\n cursor: pointer;\n}\n.filex-viewer-epub__toc-link:hover {\n background: var(--fe-bg-hover, #edf0f5);\n}\n.filex-viewer-epub__toc-link.is-child {\n padding-inline-start: 28px;\n color: var(--fe-text-muted, #5a6475);\n font-size: 12px;\n}\n.filex-viewer-epub__loading {\n position: absolute;\n inset: auto 0 0 0;\n text-align: center;\n padding: 6px;\n background: rgba(0, 0, 0, 0.05);\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n.filex-viewer-btn {\n border: 1px solid var(--fe-border, #e2e6ed);\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n padding: 4px 10px;\n border-radius: 4px;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n}\n.filex-viewer-btn:hover:not(:disabled) {\n background: var(--fe-bg-hover, #edf0f5);\n}\n.filex-viewer-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n</style>\n"],"names":["EPUB_STEP_TIMEOUT_MS","props","__props","root","ref","containerRef","error","toc","fontSize","ready","loading","book","rendition","withTimeout","p","label","timer","_","reject","load","mod","tt","Epub","source","headers","res","requestFailure","nav","err","next","_a","prev","onKey","ev","onMounted","onBeforeUnmount","_b","watch","key","fallback","typeTile","computed","fileIconTile","_createElementBlock","_openBlock","_hoisted_1","_createElementVNode","_Fragment","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_toDisplayString"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent as U, ref as p, onMounted as D, onBeforeUnmount as q, watch as z, computed as B, openBlock as n, createElementBlock as a, createElementVNode as d, toDisplayString as _, unref as G, Fragment as m, renderList as E, createCommentVNode as N } from "vue";
|
|
2
|
-
import { f as K, a as P, b as Q, s as W, e as X } from "./index-
|
|
2
|
+
import { f as K, a as P, b as Q, s as W, e as X } from "./index-1RaeWGFO.js";
|
|
3
3
|
const Y = { class: "filex-viewer-ipynb" }, Z = { class: "filex-viewer-ipynb__pane" }, ee = {
|
|
4
4
|
key: 0,
|
|
5
5
|
class: "filex-viewer-fallback"
|
|
@@ -181,4 +181,4 @@ const Y = { class: "filex-viewer-ipynb" }, Z = { class: "filex-viewer-ipynb__pan
|
|
|
181
181
|
export {
|
|
182
182
|
me as default
|
|
183
183
|
};
|
|
184
|
-
//# sourceMappingURL=IpynbViewer-
|
|
184
|
+
//# sourceMappingURL=IpynbViewer-Cmz5n0VR.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IpynbViewer-COOTyOcu.js","sources":["../src/viewers/IpynbViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { sayFailure } from '../lib/errorWords';\n/**\n * IpynbViewer — Jupyter notebook (.ipynb JSON) renderer.\n *\n * Pure-JS render — no peer dependency required:\n * - code cells → `<pre><code>` (highlight.js when available)\n * - markdown cells → markdown-it when available, plain `<pre>` otherwise\n * - output cells → text (stream / display_data 'text/plain'),\n * images (display_data 'image/png' base64),\n * HTML (display_data 'text/html', sanitized via\n * element-only insertion — no script tags)\n *\n * Source list / output count counters at the top so a 200-cell\n * notebook doesn't surprise the user.\n */\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\nimport { fetchViewerText } from '../composables/useViewerFetch';\nimport { ensureHighlight } from '../composables/useMonacoLoader';\n\nconst props = defineProps<{\n url: string;\n ext: string;\n t?: (key: string) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n}>();\n\ninterface Cell {\n cell_type: 'code' | 'markdown' | 'raw' | string;\n source: string | string[];\n outputs?: any[];\n execution_count?: number | null;\n metadata?: Record<string, unknown>;\n}\n\ninterface Notebook {\n cells: Cell[];\n metadata?: { kernelspec?: { language?: string; name?: string } };\n}\n\nconst cells = ref<Cell[]>([]);\nconst language = ref<string>('python');\nconst error = ref<string | null>(null);\nconst loading = ref(true);\nconst renderedSources = ref<Map<number, string>>(new Map());\nconst renderedMarkdown = ref<Map<number, string>>(new Map());\n\nlet renderToken = 0;\nlet highlight: any = null;\nlet mdRenderer: any = null;\n\nasync function ensureMd(): Promise<any | null> {\n if (mdRenderer !== null) return mdRenderer || null;\n try {\n const mod = await import(/* @vite-ignore */ 'markdown-it');\n const Md = (mod as any).default ?? mod;\n mdRenderer = new Md({ html: false, linkify: true, breaks: true });\n return mdRenderer;\n } catch {\n mdRenderer = false;\n return null;\n }\n}\n\nfunction joinSource(source: string | string[]): string {\n return Array.isArray(source) ? source.join('') : (source ?? '');\n}\n\nasync function load(): Promise<void> {\n loading.value = true;\n error.value = null;\n cells.value = [];\n renderedSources.value = new Map();\n renderedMarkdown.value = new Map();\n const myToken = ++renderToken;\n\n let raw: string;\n try {\n raw = await fetchViewerText({\n url: props.url,\n headers: (await props.authHeaders?.()) ?? {},\n credentials: props.authCredentials,\n });\n } catch (err) {\n error.value = sayFailure(err, tt('viewer.failed_to_load', 'Failed to load file'), { t: props.t }).text;\n loading.value = false;\n return;\n }\n\n if (myToken !== renderToken) return;\n\n let nb: Notebook;\n try {\n nb = JSON.parse(raw);\n } catch (err) {\n error.value = 'Invalid notebook JSON';\n loading.value = false;\n return;\n }\n cells.value = Array.isArray(nb.cells) ? nb.cells : [];\n language.value = nb.metadata?.kernelspec?.language || 'python';\n\n // Best-effort hljs highlight per code cell.\n highlight = (await ensureHighlight()) as any;\n if (myToken !== renderToken) return;\n if (highlight) {\n const hljs = highlight.default ?? highlight;\n const langOk = hljs.getLanguage?.(language.value) ? language.value : null;\n cells.value.forEach((cell, idx) => {\n if (cell.cell_type === 'code') {\n const text = joinSource(cell.source);\n try {\n const result = langOk\n ? hljs.highlight(text, { language: langOk, ignoreIllegals: true })\n : hljs.highlightAuto(text);\n renderedSources.value.set(idx, result.value);\n } catch {\n /* leave unrendered */\n }\n }\n });\n }\n\n const md = await ensureMd();\n if (myToken !== renderToken) return;\n if (md) {\n cells.value.forEach((cell, idx) => {\n if (cell.cell_type === 'markdown') {\n try {\n renderedMarkdown.value.set(idx, md.render(joinSource(cell.source)));\n } catch {\n /* leave plain */\n }\n }\n });\n }\n\n loading.value = false;\n}\n\nfunction outputText(out: any): string | null {\n if (!out) return null;\n if (out.output_type === 'stream') {\n return joinSource(out.text);\n }\n if (out.output_type === 'error') {\n return [out.ename, out.evalue, ...(out.traceback || [])]\n .filter(Boolean)\n .join('\\n');\n }\n if (out.output_type === 'execute_result' || out.output_type === 'display_data') {\n if (out.data?.['text/plain']) {\n return joinSource(out.data['text/plain']);\n }\n }\n return null;\n}\n\nfunction outputImage(out: any): string | null {\n if (!out?.data) return null;\n if (out.data['image/png']) return `data:image/png;base64,${out.data['image/png']}`;\n if (out.data['image/jpeg']) return `data:image/jpeg;base64,${out.data['image/jpeg']}`;\n if (out.data['image/svg+xml']) {\n const svg = joinSource(out.data['image/svg+xml']);\n return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;\n }\n return null;\n}\n\nfunction outputHtml(out: any): string | null {\n if (!out?.data) return null;\n if (out.data['text/html']) {\n const html = joinSource(out.data['text/html']);\n // Strip <script> for safety — outputs from arbitrary notebooks.\n return html.replace(/<script[\\s\\S]*?<\\/script>/gi, '');\n }\n return null;\n}\n\nonMounted(load);\nonBeforeUnmount(() => {\n renderToken++;\n});\nwatch(() => props.url, load);\n\nconst stats = computed(() => {\n const codeCount = cells.value.filter((c) => c.cell_type === 'code').length;\n const mdCount = cells.value.filter((c) => c.cell_type === 'markdown').length;\n return { code: codeCount, md: mdCount, total: cells.value.length };\n});\n\nfunction tt(key: string, fallback: string): string {\n return props.t ? props.t(key) : fallback;\n}\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n</script>\n\n<template>\n <div class=\"filex-viewer-ipynb\">\n <div class=\"filex-viewer-ipynb__pane\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <div v-else-if=\"loading\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span\n class=\"filex-viewer-fallback__icon filex-viewer-fallback__icon--spin\"\n aria-hidden=\"true\"\n v-html=\"actionIconSvg('progress')\"\n ></span>\n <p>{{ tt('viewer.loading', 'Loading…') }}</p>\n </div>\n <div v-else>\n <div\n v-for=\"(cell, idx) in cells\"\n :key=\"idx\"\n class=\"filex-viewer-ipynb__cell\"\n :data-type=\"cell.cell_type\"\n >\n <template v-if=\"cell.cell_type === 'code'\">\n <div class=\"filex-viewer-ipynb__exec\">\n In [{{ cell.execution_count ?? ' ' }}]:\n </div>\n <pre class=\"filex-viewer-ipynb__src hljs\"><code\n v-if=\"renderedSources.get(idx)\"\n v-html=\"renderedSources.get(idx)\"\n /><code v-else>{{ joinSource(cell.source) }}</code></pre>\n <div v-if=\"cell.outputs && cell.outputs.length\" class=\"filex-viewer-ipynb__outputs\">\n <template v-for=\"(out, j) in cell.outputs\" :key=\"j\">\n <img\n v-if=\"outputImage(out)\"\n :src=\"outputImage(out) || ''\"\n class=\"filex-viewer-ipynb__img\"\n :alt=\"`output ${j}`\"\n />\n <div\n v-else-if=\"outputHtml(out)\"\n class=\"filex-viewer-ipynb__html\"\n v-html=\"outputHtml(out)\"\n />\n <pre\n v-else-if=\"outputText(out)\"\n class=\"filex-viewer-ipynb__out\"\n :data-type=\"out.output_type\"\n >{{ outputText(out) }}</pre>\n </template>\n </div>\n </template>\n\n <template v-else-if=\"cell.cell_type === 'markdown'\">\n <div\n v-if=\"renderedMarkdown.get(idx)\"\n class=\"filex-viewer-ipynb__md\"\n dir=\"auto\"\n v-html=\"renderedMarkdown.get(idx)\"\n />\n <pre v-else class=\"filex-viewer-ipynb__md-raw\">{{ joinSource(cell.source) }}</pre>\n </template>\n\n <template v-else>\n <pre class=\"filex-viewer-ipynb__src\">{{ joinSource(cell.source) }}</pre>\n </template>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-ipynb {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n}\n.filex-viewer-ipynb__bar {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-bottom: 1px solid var(--fe-border, #e2e6ed);\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-ipynb__lang {\n font-family: var(--fe-font-mono, monospace);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n.filex-viewer-ipynb__pane {\n flex: 1;\n overflow: auto;\n padding: 16px 24px;\n}\n.filex-viewer-ipynb__cell {\n border: 1px solid var(--fe-border, #e2e6ed);\n border-radius: 6px;\n margin-bottom: 16px;\n overflow: hidden;\n}\n.filex-viewer-ipynb__cell[data-type=\"code\"] {\n background: var(--fe-bg-elev, #f7f8fa);\n}\n.filex-viewer-ipynb__exec {\n padding: 4px 12px;\n font-family: var(--fe-font-mono, monospace);\n font-size: 11px;\n color: var(--fe-text-muted, #5a6475);\n background: rgba(0, 0, 0, 0.04);\n}\n.filex-viewer-ipynb__src {\n margin: 0;\n padding: 12px;\n font-family: var(--fe-font-mono, monospace);\n font-size: 12px;\n line-height: 1.5;\n white-space: pre;\n overflow-x: auto;\n}\n.filex-viewer-ipynb__outputs {\n border-top: 1px dashed var(--fe-border, #e2e6ed);\n background: var(--fe-bg, #fff);\n padding: 8px 12px;\n}\n.filex-viewer-ipynb__out {\n margin: 0;\n padding: 4px 0;\n font-family: var(--fe-font-mono, monospace);\n font-size: 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n}\n.filex-viewer-ipynb__out[data-type=\"error\"] {\n color: var(--fe-danger, #dc2626);\n}\n.filex-viewer-ipynb__img {\n display: block;\n max-width: 100%;\n height: auto;\n margin: 8px 0;\n}\n.filex-viewer-ipynb__html {\n font-size: 13px;\n}\n.filex-viewer-ipynb__md {\n padding: 12px 16px;\n font-size: 14px;\n line-height: 1.6;\n}\n.filex-viewer-ipynb__md-raw {\n padding: 12px;\n margin: 0;\n font-family: var(--fe-font-mono, monospace);\n font-size: 12px;\n white-space: pre-wrap;\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n</style>\n"],"names":["props","__props","cells","ref","language","error","loading","renderedSources","renderedMarkdown","renderToken","highlight","mdRenderer","ensureMd","mod","Md","joinSource","source","load","myToken","raw","fetchViewerText","_a","err","sayFailure","tt","nb","_c","_b","ensureHighlight","hljs","langOk","_d","cell","idx","text","result","md","outputText","out","outputImage","svg","outputHtml","onMounted","onBeforeUnmount","watch","computed","codeCount","c","mdCount","key","fallback","typeTile","fileIconTile","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_hoisted_5","_unref","actionIconSvg","_hoisted_7","_Fragment","_renderList","_hoisted_9","_hoisted_10","_hoisted_12","_toDisplayString","_hoisted_13","j","_hoisted_16","_hoisted_18","_hoisted_19"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"IpynbViewer-Cmz5n0VR.js","sources":["../src/viewers/IpynbViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { sayFailure } from '../lib/errorWords';\n/**\n * IpynbViewer — Jupyter notebook (.ipynb JSON) renderer.\n *\n * Pure-JS render — no peer dependency required:\n * - code cells → `<pre><code>` (highlight.js when available)\n * - markdown cells → markdown-it when available, plain `<pre>` otherwise\n * - output cells → text (stream / display_data 'text/plain'),\n * images (display_data 'image/png' base64),\n * HTML (display_data 'text/html', sanitized via\n * element-only insertion — no script tags)\n *\n * Source list / output count counters at the top so a 200-cell\n * notebook doesn't surprise the user.\n */\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */\nimport { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */\nimport { fetchViewerText } from '../composables/useViewerFetch';\nimport { ensureHighlight } from '../composables/useMonacoLoader';\n\nconst props = defineProps<{\n url: string;\n ext: string;\n t?: (key: string) => string;\n authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;\n authCredentials?: RequestCredentials;\n}>();\n\ninterface Cell {\n cell_type: 'code' | 'markdown' | 'raw' | string;\n source: string | string[];\n outputs?: any[];\n execution_count?: number | null;\n metadata?: Record<string, unknown>;\n}\n\ninterface Notebook {\n cells: Cell[];\n metadata?: { kernelspec?: { language?: string; name?: string } };\n}\n\nconst cells = ref<Cell[]>([]);\nconst language = ref<string>('python');\nconst error = ref<string | null>(null);\nconst loading = ref(true);\nconst renderedSources = ref<Map<number, string>>(new Map());\nconst renderedMarkdown = ref<Map<number, string>>(new Map());\n\nlet renderToken = 0;\nlet highlight: any = null;\nlet mdRenderer: any = null;\n\nasync function ensureMd(): Promise<any | null> {\n if (mdRenderer !== null) return mdRenderer || null;\n try {\n const mod = await import(/* @vite-ignore */ 'markdown-it');\n const Md = (mod as any).default ?? mod;\n mdRenderer = new Md({ html: false, linkify: true, breaks: true });\n return mdRenderer;\n } catch {\n mdRenderer = false;\n return null;\n }\n}\n\nfunction joinSource(source: string | string[]): string {\n return Array.isArray(source) ? source.join('') : (source ?? '');\n}\n\nasync function load(): Promise<void> {\n loading.value = true;\n error.value = null;\n cells.value = [];\n renderedSources.value = new Map();\n renderedMarkdown.value = new Map();\n const myToken = ++renderToken;\n\n let raw: string;\n try {\n raw = await fetchViewerText({\n url: props.url,\n headers: (await props.authHeaders?.()) ?? {},\n credentials: props.authCredentials,\n });\n } catch (err) {\n error.value = sayFailure(err, tt('viewer.failed_to_load', 'Failed to load file'), { t: props.t }).text;\n loading.value = false;\n return;\n }\n\n if (myToken !== renderToken) return;\n\n let nb: Notebook;\n try {\n nb = JSON.parse(raw);\n } catch (err) {\n error.value = 'Invalid notebook JSON';\n loading.value = false;\n return;\n }\n cells.value = Array.isArray(nb.cells) ? nb.cells : [];\n language.value = nb.metadata?.kernelspec?.language || 'python';\n\n // Best-effort hljs highlight per code cell.\n highlight = (await ensureHighlight()) as any;\n if (myToken !== renderToken) return;\n if (highlight) {\n const hljs = highlight.default ?? highlight;\n const langOk = hljs.getLanguage?.(language.value) ? language.value : null;\n cells.value.forEach((cell, idx) => {\n if (cell.cell_type === 'code') {\n const text = joinSource(cell.source);\n try {\n const result = langOk\n ? hljs.highlight(text, { language: langOk, ignoreIllegals: true })\n : hljs.highlightAuto(text);\n renderedSources.value.set(idx, result.value);\n } catch {\n /* leave unrendered */\n }\n }\n });\n }\n\n const md = await ensureMd();\n if (myToken !== renderToken) return;\n if (md) {\n cells.value.forEach((cell, idx) => {\n if (cell.cell_type === 'markdown') {\n try {\n renderedMarkdown.value.set(idx, md.render(joinSource(cell.source)));\n } catch {\n /* leave plain */\n }\n }\n });\n }\n\n loading.value = false;\n}\n\nfunction outputText(out: any): string | null {\n if (!out) return null;\n if (out.output_type === 'stream') {\n return joinSource(out.text);\n }\n if (out.output_type === 'error') {\n return [out.ename, out.evalue, ...(out.traceback || [])]\n .filter(Boolean)\n .join('\\n');\n }\n if (out.output_type === 'execute_result' || out.output_type === 'display_data') {\n if (out.data?.['text/plain']) {\n return joinSource(out.data['text/plain']);\n }\n }\n return null;\n}\n\nfunction outputImage(out: any): string | null {\n if (!out?.data) return null;\n if (out.data['image/png']) return `data:image/png;base64,${out.data['image/png']}`;\n if (out.data['image/jpeg']) return `data:image/jpeg;base64,${out.data['image/jpeg']}`;\n if (out.data['image/svg+xml']) {\n const svg = joinSource(out.data['image/svg+xml']);\n return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;\n }\n return null;\n}\n\nfunction outputHtml(out: any): string | null {\n if (!out?.data) return null;\n if (out.data['text/html']) {\n const html = joinSource(out.data['text/html']);\n // Strip <script> for safety — outputs from arbitrary notebooks.\n return html.replace(/<script[\\s\\S]*?<\\/script>/gi, '');\n }\n return null;\n}\n\nonMounted(load);\nonBeforeUnmount(() => {\n renderToken++;\n});\nwatch(() => props.url, load);\n\nconst stats = computed(() => {\n const codeCount = cells.value.filter((c) => c.cell_type === 'code').length;\n const mdCount = cells.value.filter((c) => c.cell_type === 'markdown').length;\n return { code: codeCount, md: mdCount, total: cells.value.length };\n});\n\nfunction tt(key: string, fallback: string): string {\n return props.t ? props.t(key) : fallback;\n}\n\n/* === ikon:emoji — the fallback screen's mark ==========================\n * Every viewer opened its \"cannot show this\" / \"still loading\" screen with a\n * 48px colour emoji, one per format, each from whatever emoji font the OS\n * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row\n * the person just clicked is wearing, so the fallback is recognisably about\n * that file — and \"loading\" is the stroked ring, spun by CSS, because no\n * still picture can say \"still going\". */\nconst typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));\n</script>\n\n<template>\n <div class=\"filex-viewer-ipynb\">\n <div class=\"filex-viewer-ipynb__pane\">\n <div v-if=\"error\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span class=\"filex-viewer-fallback__icon\" aria-hidden=\"true\" v-html=\"typeTile\"></span>\n <p>{{ error }}</p>\n </div>\n <div v-else-if=\"loading\" class=\"filex-viewer-fallback\">\n <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->\n <span\n class=\"filex-viewer-fallback__icon filex-viewer-fallback__icon--spin\"\n aria-hidden=\"true\"\n v-html=\"actionIconSvg('progress')\"\n ></span>\n <p>{{ tt('viewer.loading', 'Loading…') }}</p>\n </div>\n <div v-else>\n <div\n v-for=\"(cell, idx) in cells\"\n :key=\"idx\"\n class=\"filex-viewer-ipynb__cell\"\n :data-type=\"cell.cell_type\"\n >\n <template v-if=\"cell.cell_type === 'code'\">\n <div class=\"filex-viewer-ipynb__exec\">\n In [{{ cell.execution_count ?? ' ' }}]:\n </div>\n <pre class=\"filex-viewer-ipynb__src hljs\"><code\n v-if=\"renderedSources.get(idx)\"\n v-html=\"renderedSources.get(idx)\"\n /><code v-else>{{ joinSource(cell.source) }}</code></pre>\n <div v-if=\"cell.outputs && cell.outputs.length\" class=\"filex-viewer-ipynb__outputs\">\n <template v-for=\"(out, j) in cell.outputs\" :key=\"j\">\n <img\n v-if=\"outputImage(out)\"\n :src=\"outputImage(out) || ''\"\n class=\"filex-viewer-ipynb__img\"\n :alt=\"`output ${j}`\"\n />\n <div\n v-else-if=\"outputHtml(out)\"\n class=\"filex-viewer-ipynb__html\"\n v-html=\"outputHtml(out)\"\n />\n <pre\n v-else-if=\"outputText(out)\"\n class=\"filex-viewer-ipynb__out\"\n :data-type=\"out.output_type\"\n >{{ outputText(out) }}</pre>\n </template>\n </div>\n </template>\n\n <template v-else-if=\"cell.cell_type === 'markdown'\">\n <div\n v-if=\"renderedMarkdown.get(idx)\"\n class=\"filex-viewer-ipynb__md\"\n dir=\"auto\"\n v-html=\"renderedMarkdown.get(idx)\"\n />\n <pre v-else class=\"filex-viewer-ipynb__md-raw\">{{ joinSource(cell.source) }}</pre>\n </template>\n\n <template v-else>\n <pre class=\"filex-viewer-ipynb__src\">{{ joinSource(cell.source) }}</pre>\n </template>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style>\n/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to\n `.cls[data-v-HASH]`, and in the web-component build the hash baked into\n this CSS does not match the one Vue stamps onto the DOM — so every rule\n here silently stopped applying. Measured in the desktop app: the share\n dialog had `position: static`, no background and no radius, i.e. raw\n unstyled HTML, in EVERY embedded surface.\n Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so\n there is nothing here that can leak into a host page. */\n.filex-viewer-ipynb {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 70vh;\n background: var(--fe-bg, #fff);\n color: var(--fe-text, #1a1e27);\n}\n.filex-viewer-ipynb__bar {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n background: var(--fe-bg-elev, #f7f8fa);\n border-bottom: 1px solid var(--fe-border, #e2e6ed);\n font-size: 12px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-ipynb__lang {\n font-family: var(--fe-font-mono, monospace);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n.filex-viewer-ipynb__pane {\n flex: 1;\n overflow: auto;\n padding: 16px 24px;\n}\n.filex-viewer-ipynb__cell {\n border: 1px solid var(--fe-border, #e2e6ed);\n border-radius: 6px;\n margin-bottom: 16px;\n overflow: hidden;\n}\n.filex-viewer-ipynb__cell[data-type=\"code\"] {\n background: var(--fe-bg-elev, #f7f8fa);\n}\n.filex-viewer-ipynb__exec {\n padding: 4px 12px;\n font-family: var(--fe-font-mono, monospace);\n font-size: 11px;\n color: var(--fe-text-muted, #5a6475);\n background: rgba(0, 0, 0, 0.04);\n}\n.filex-viewer-ipynb__src {\n margin: 0;\n padding: 12px;\n font-family: var(--fe-font-mono, monospace);\n font-size: 12px;\n line-height: 1.5;\n white-space: pre;\n overflow-x: auto;\n}\n.filex-viewer-ipynb__outputs {\n border-top: 1px dashed var(--fe-border, #e2e6ed);\n background: var(--fe-bg, #fff);\n padding: 8px 12px;\n}\n.filex-viewer-ipynb__out {\n margin: 0;\n padding: 4px 0;\n font-family: var(--fe-font-mono, monospace);\n font-size: 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n}\n.filex-viewer-ipynb__out[data-type=\"error\"] {\n color: var(--fe-danger, #dc2626);\n}\n.filex-viewer-ipynb__img {\n display: block;\n max-width: 100%;\n height: auto;\n margin: 8px 0;\n}\n.filex-viewer-ipynb__html {\n font-size: 13px;\n}\n.filex-viewer-ipynb__md {\n padding: 12px 16px;\n font-size: 14px;\n line-height: 1.6;\n}\n.filex-viewer-ipynb__md-raw {\n padding: 12px;\n margin: 0;\n font-family: var(--fe-font-mono, monospace);\n font-size: 12px;\n white-space: pre-wrap;\n}\n.filex-viewer-fallback {\n text-align: center;\n padding: 32px;\n color: var(--fe-text-muted, #5a6475);\n}\n.filex-viewer-fallback__icon {\n font-size: 48px;\n display: block;\n margin-bottom: 12px;\n}\n</style>\n"],"names":["props","__props","cells","ref","language","error","loading","renderedSources","renderedMarkdown","renderToken","highlight","mdRenderer","ensureMd","mod","Md","joinSource","source","load","myToken","raw","fetchViewerText","_a","err","sayFailure","tt","nb","_c","_b","ensureHighlight","hljs","langOk","_d","cell","idx","text","result","md","outputText","out","outputImage","svg","outputHtml","onMounted","onBeforeUnmount","watch","computed","codeCount","c","mdCount","key","fallback","typeTile","fileIconTile","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_hoisted_5","_unref","actionIconSvg","_hoisted_7","_Fragment","_renderList","_hoisted_9","_hoisted_10","_hoisted_12","_toDisplayString","_hoisted_13","j","_hoisted_16","_hoisted_18","_hoisted_19"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent as V, ref as s, onMounted as $, onBeforeUnmount as B, computed as E, openBlock as w, createElementBlock as x, createElementVNode as u, toDisplayString as M, unref as I, createCommentVNode as Y, withDirectives as z, normalizeStyle as K, vShow as N } from "vue";
|
|
2
|
-
import { f as U, a as W, b as X, s as T } from "./index-
|
|
2
|
+
import { f as U, a as W, b as X, s as T } from "./index-1RaeWGFO.js";
|
|
3
3
|
const q = { class: "filex-viewer-mermaid" }, O = {
|
|
4
4
|
key: 0,
|
|
5
5
|
class: "filex-viewer-fallback"
|
|
@@ -133,4 +133,4 @@ const q = { class: "filex-viewer-mermaid" }, O = {
|
|
|
133
133
|
export {
|
|
134
134
|
R as default
|
|
135
135
|
};
|
|
136
|
-
//# sourceMappingURL=MermaidViewer-
|
|
136
|
+
//# sourceMappingURL=MermaidViewer-BJ5uOyhc.js.map
|