@l1yp/file-viewer 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +55 -6
  2. package/dist/assets/step.worker-CfDTYOf_.js.map +1 -0
  3. package/dist/components/CertViewer.js +1 -1
  4. package/dist/components/CertViewer.js.map +1 -1
  5. package/dist/components/CertViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  6. package/dist/components/DocxNavigationPane.js +1 -1
  7. package/dist/components/DocxNavigationPane.js.map +1 -1
  8. package/dist/components/DocxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
  9. package/dist/components/DocxViewer.js +1 -1
  10. package/dist/components/DocxViewer.js.map +1 -1
  11. package/dist/components/DocxViewer.vue.d.ts +1 -1
  12. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js +80 -78
  13. package/dist/components/DocxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  14. package/dist/components/EplViewer.js +1 -1
  15. package/dist/components/EplViewer.js.map +1 -1
  16. package/dist/components/EplViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  17. package/dist/components/FileViewerBody.js +1 -1
  18. package/dist/components/FileViewerBody.js.map +1 -1
  19. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js +265 -257
  20. package/dist/components/FileViewerBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
  21. package/dist/components/FileViewerPanel.js +1 -1
  22. package/dist/components/FileViewerPanel.js.map +1 -1
  23. package/dist/components/FileViewerPanel.vue.d.ts +6 -1
  24. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js +42 -40
  25. package/dist/components/FileViewerPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  26. package/dist/components/HarViewer.js +1 -1
  27. package/dist/components/HarViewer.js.map +1 -1
  28. package/dist/components/HarViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  29. package/dist/components/JksViewer.js +1 -1
  30. package/dist/components/JksViewer.js.map +1 -1
  31. package/dist/components/JksViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  32. package/dist/components/ModelViewModes.js +9 -0
  33. package/dist/components/ModelViewModes.js.map +1 -0
  34. package/dist/components/ModelViewModes.vue.d.ts +11 -0
  35. package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js +49 -0
  36. package/dist/components/ModelViewModes.vue_vue_type_script_setup_true_lang.js.map +1 -0
  37. package/dist/components/ModelViewer.js +1 -1
  38. package/dist/components/ModelViewer.js.map +1 -1
  39. package/dist/components/ModelViewer.vue.d.ts +2 -1
  40. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js +183 -96
  41. package/dist/components/ModelViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  42. package/dist/components/PdfNavigationPane.js +1 -1
  43. package/dist/components/PdfNavigationPane.js.map +1 -1
  44. package/dist/components/PdfNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
  45. package/dist/components/PdfViewer.js +1 -1
  46. package/dist/components/PdfViewer.js.map +1 -1
  47. package/dist/components/PdfViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  48. package/dist/components/PptxNavigationPane.js +1 -1
  49. package/dist/components/PptxNavigationPane.js.map +1 -1
  50. package/dist/components/PptxNavigationPane.vue_vue_type_script_setup_true_lang.js.map +1 -1
  51. package/dist/components/PsdViewer.js +1 -1
  52. package/dist/components/PsdViewer.js.map +1 -1
  53. package/dist/components/PsdViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  54. package/dist/components/SqliteViewer.js +1 -1
  55. package/dist/components/SqliteViewer.js.map +1 -1
  56. package/dist/components/SqliteViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  57. package/dist/components/ViewerLoadError.js +9 -0
  58. package/dist/components/ViewerLoadError.js.map +1 -0
  59. package/dist/components/ViewerLoadError.vue.d.ts +17 -0
  60. package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js +58 -0
  61. package/dist/components/ViewerLoadError.vue_vue_type_script_setup_true_lang.js.map +1 -0
  62. package/dist/components/XlsxChart.js +1 -1
  63. package/dist/components/XlsxChart.js.map +1 -1
  64. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js +9 -9
  65. package/dist/components/XlsxChart.vue_vue_type_script_setup_true_lang.js.map +1 -1
  66. package/dist/components/XlsxImageLayer.js +9 -0
  67. package/dist/components/XlsxImageLayer.js.map +1 -0
  68. package/dist/components/XlsxImageLayer.vue.d.ts +9 -0
  69. package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js +46 -0
  70. package/dist/components/XlsxImageLayer.vue_vue_type_script_setup_true_lang.js.map +1 -0
  71. package/dist/components/XlsxViewer.js +1 -1
  72. package/dist/components/XlsxViewer.js.map +1 -1
  73. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js +230 -216
  74. package/dist/components/XlsxViewer.vue_vue_type_script_setup_true_lang.js.map +1 -1
  75. package/dist/components/epl/EplResource.js +1 -1
  76. package/dist/components/epl/EplResource.js.map +1 -1
  77. package/dist/components/epl/EplResource.vue_vue_type_script_setup_true_lang.js.map +1 -1
  78. package/dist/components/optionalViewer.d.ts +3 -2
  79. package/dist/components/optionalViewer.js +6 -3
  80. package/dist/components/optionalViewer.js.map +1 -1
  81. package/dist/composables/useFileViewer.d.ts +11 -3
  82. package/dist/composables/useFileViewer.js.map +1 -1
  83. package/dist/composables/useTeleportTheme.d.ts +7 -0
  84. package/dist/composables/useTeleportTheme.js +42 -0
  85. package/dist/composables/useTeleportTheme.js.map +1 -0
  86. package/dist/index.d.ts +1 -0
  87. package/dist/index.js +29 -28
  88. package/dist/lib/docxTheme.d.ts +6 -0
  89. package/dist/lib/docxTheme.js +64 -0
  90. package/dist/lib/docxTheme.js.map +1 -0
  91. package/dist/lib/epl/code/structures.js +82 -0
  92. package/dist/lib/epl/code/structures.js.map +1 -0
  93. package/dist/lib/epl/code/variable.js +16 -0
  94. package/dist/lib/epl/code/variable.js.map +1 -0
  95. package/dist/lib/epl/eplDocument.js +28 -22
  96. package/dist/lib/epl/eplDocument.js.map +1 -1
  97. package/dist/lib/epl/index.d.ts +2 -1
  98. package/dist/lib/epl/index.js.map +1 -1
  99. package/dist/lib/epl/sections/code.js +37 -0
  100. package/dist/lib/epl/sections/code.js.map +1 -0
  101. package/dist/lib/epl/sections/esystemInfo.js +29 -0
  102. package/dist/lib/epl/sections/esystemInfo.js.map +1 -0
  103. package/dist/lib/epl/sections/losable.js +33 -0
  104. package/dist/lib/epl/sections/losable.js.map +1 -0
  105. package/dist/lib/epl/sections/projectConfig.js +39 -0
  106. package/dist/lib/epl/sections/projectConfig.js.map +1 -0
  107. package/dist/lib/epl/sections/resource.js +84 -0
  108. package/dist/lib/epl/sections/resource.js.map +1 -0
  109. package/dist/lib/fileType.d.ts +3 -1
  110. package/dist/lib/fileType.js +150 -137
  111. package/dist/lib/fileType.js.map +1 -1
  112. package/dist/lib/modelAppearance.d.ts +7 -0
  113. package/dist/lib/modelAppearance.js +41 -0
  114. package/dist/lib/modelAppearance.js.map +1 -0
  115. package/dist/lib/step.d.ts +10 -0
  116. package/dist/lib/step.js +35 -0
  117. package/dist/lib/step.js.map +1 -0
  118. package/dist/lib/step.worker.d.ts +1 -0
  119. package/dist/lib/step.worker.js +18 -0
  120. package/dist/lib/step.worker.js.map +1 -0
  121. package/dist/lib/stepObject.d.ts +4 -0
  122. package/dist/lib/stepObject.js +52 -0
  123. package/dist/lib/stepObject.js.map +1 -0
  124. package/dist/lib/stepTypes.d.ts +60 -0
  125. package/dist/lib/viewerKind.js +10 -5
  126. package/dist/lib/viewerKind.js.map +1 -1
  127. package/dist/lib/xlsx.d.ts +1 -1
  128. package/dist/lib/xlsx.js +106 -89
  129. package/dist/lib/xlsx.js.map +1 -1
  130. package/dist/lib/xlsxCharts.js +71 -119
  131. package/dist/lib/xlsxCharts.js.map +1 -1
  132. package/dist/lib/xlsxImages.d.ts +43 -0
  133. package/dist/lib/xlsxImages.js +114 -0
  134. package/dist/lib/xlsxImages.js.map +1 -0
  135. package/dist/lib/xlsxParts.d.ts +17 -0
  136. package/dist/lib/xlsxParts.js +86 -0
  137. package/dist/lib/xlsxParts.js.map +1 -0
  138. package/dist/style.css +1 -1
  139. package/package.json +8 -3
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the
3
3
  * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a
4
- * paginated, paper-like preview (white pages on a neutral canvas, like Word). No CDN / network beyond the
4
+ * paginated preview (plain pages follow the viewer; authored colors keep their paper). No CDN / network beyond the
5
5
  * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.
6
6
  *
7
7
  * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a
@@ -6,23 +6,24 @@ import i from "./DocxNavigationPane.js";
6
6
  import { useElementNavigation as a } from "../composables/useElementNavigation.js";
7
7
  import { extractDocxEmbeddedFile as o, readDocxEmbeddedFiles as s } from "../lib/docxEmbeddings.js";
8
8
  import { mountDocxEmbeddedPreviews as c } from "../lib/docxEmbeddedDom.js";
9
- import { buildDocxNavigation as ee, buildDocxPageNavigation as te } from "../lib/docxNavigation.js";
10
- import { Fragment as l, createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createVNode as m, defineAsyncComponent as h, defineComponent as g, nextTick as ne, onBeforeUnmount as re, openBlock as _, shallowRef as v, toDisplayString as y, unref as b, vShow as ie, watch as ae, withDirectives as oe } from "vue";
11
- import { renderAsync as se } from "docx-preview";
9
+ import { buildDocxNavigation as ee, buildDocxPageNavigation as l } from "../lib/docxNavigation.js";
10
+ import { applyDocxReadingTheme as te } from "../lib/docxTheme.js";
11
+ import { Fragment as u, createBlock as d, createCommentVNode as f, createElementBlock as p, createElementVNode as m, createVNode as h, defineAsyncComponent as ne, defineComponent as g, nextTick as re, onBeforeUnmount as ie, openBlock as _, shallowRef as v, toDisplayString as y, unref as b, vShow as ae, watch as x, withDirectives as S } from "vue";
12
+ import { renderAsync as C } from "docx-preview";
12
13
  //#region src/components/DocxViewer.vue?vue&type=script&setup=true&lang.ts
13
- var ce = { class: "dx" }, le = { class: "dx__stage" }, x = {
14
+ var oe = { class: "dx" }, se = { class: "dx__stage" }, ce = {
14
15
  key: 0,
15
16
  class: "dx__embedded-bar"
16
- }, S = ["title"], C = { class: "dx__embedded-size" }, ue = {
17
+ }, le = ["title"], ue = { class: "dx__embedded-size" }, de = {
17
18
  key: 1,
18
19
  class: "dx__msg"
19
- }, de = {
20
+ }, fe = {
20
21
  key: 2,
21
22
  class: "dx__msg dx__msg--error"
22
- }, fe = {
23
+ }, pe = {
23
24
  key: 0,
24
25
  class: "dx__msg"
25
- }, pe = {
26
+ }, me = {
26
27
  key: 1,
27
28
  class: "dx__msg dx__msg--error"
28
29
  }, w = /*@__PURE__*/ g({
@@ -33,34 +34,34 @@ var ce = { class: "dx" }, le = { class: "dx__stage" }, x = {
33
34
  size: {}
34
35
  },
35
36
  setup(g) {
36
- let w = h(() => import("./FileViewerBody.js")), T = g, E = v("loading"), D = v(""), O = v(null), k = v("outline"), A = v(!1), { outlineItems: me, pageItems: he, activeOutlineId: j, activePageId: M, setItems: N, navigate: P } = a(O), F = null, I = "", L = 0, R = 0, z = 0, B = null, V = v([]), H = v(null), U = v(null), W = v("ready"), G = v(""), K = v(""), q = v("");
37
+ let w = ne(() => import("./FileViewerBody.js")), T = g, E = v("loading"), D = v(""), O = v(null), k = v("outline"), A = v(!1), { outlineItems: he, pageItems: j, activeOutlineId: M, activePageId: N, setItems: P, navigate: F } = a(O), I = null, L = "", R = 0, z = 0, B = 0, V = null, H = v([]), U = v(null), W = v(null), G = v("ready"), K = v(""), q = v(""), J = v("");
37
38
  function ge() {
38
- I &&= (URL.revokeObjectURL(I), "");
39
+ L &&= (URL.revokeObjectURL(L), "");
39
40
  }
40
- function J() {
41
- B?.dispose(), B = null;
41
+ function _e() {
42
+ V?.dispose(), V = null;
42
43
  }
43
44
  function Y(e = !0) {
44
- let t = e && H.value !== null;
45
- L++, H.value = null, U.value = null, W.value = "ready", G.value = "", ge(), t && ne(() => {
46
- !H.value && O.value && (O.value.scrollTop = z);
45
+ let t = e && U.value !== null;
46
+ R++, U.value = null, W.value = null, G.value = "ready", K.value = "", ge(), t && re(() => {
47
+ !U.value && O.value && (O.value.scrollTop = B);
47
48
  });
48
49
  }
49
50
  function X() {
50
- Y(!1), J(), F = null, z = 0, V.value = [], K.value = "", q.value = "";
51
+ Y(!1), _e(), I = null, B = 0, H.value = [], q.value = "", J.value = "";
51
52
  }
52
53
  async function Z() {
53
54
  let e = O.value;
54
55
  if (!T.url || !e) return;
55
- let t = ++R;
56
- E.value = "loading", D.value = "", X(), N([], []), e.innerHTML = "";
56
+ let t = ++z;
57
+ E.value = "loading", D.value = "", X(), P([], []), e.innerHTML = "";
57
58
  let n = T.url;
58
59
  try {
59
60
  let r = await fetch(n);
60
61
  if (!r.ok) throw Error(`HTTP ${r.status}`);
61
62
  let i = await r.arrayBuffer();
62
- if (R !== t || T.url !== n || O.value !== e) return;
63
- let a = s(i).catch((e) => (console.warn("DOCX embedded files unavailable:", e), [])), o = await se(i, e, void 0, {
63
+ if (z !== t || T.url !== n || O.value !== e) return;
64
+ let a = s(i).catch((e) => (console.warn("DOCX embedded files unavailable:", e), [])), o = await C(i, e, void 0, {
64
65
  className: "docx",
65
66
  inWrapper: !0,
66
67
  ignoreWidth: !0,
@@ -71,37 +72,38 @@ var ce = { class: "dx" }, le = { class: "dx__stage" }, x = {
71
72
  renderFootnotes: !0,
72
73
  renderEndnotes: !0,
73
74
  useBase64URL: !0
74
- }), l = await a;
75
- if (R !== t || T.url !== n || O.value !== e) return;
76
- let u;
75
+ }), u = await a;
76
+ if (z !== t || T.url !== n || O.value !== e) return;
77
+ te(e);
78
+ let d;
77
79
  try {
78
- u = ee(o, e);
80
+ d = ee(o, e);
79
81
  } catch (t) {
80
- console.warn("DOCX outline navigation unavailable:", t), u = {
82
+ console.warn("DOCX outline navigation unavailable:", t), d = {
81
83
  outline: [],
82
- pages: te(e)
84
+ pages: l(e)
83
85
  };
84
86
  }
85
- N(u.outline, u.pages), k.value = u.outline.length ? "outline" : "pages", F = i, V.value = l, B = c({
87
+ P(d.outline, d.pages), k.value = d.outline.length ? "outline" : "pages", I = i, H.value = u, V = c({
86
88
  root: e,
87
89
  buffer: i,
88
- files: l,
90
+ files: u,
89
91
  onOpen: (e) => void Q(e)
90
92
  }), E.value = "ready";
91
93
  } catch (e) {
92
- if (R !== t) return;
94
+ if (z !== t) return;
93
95
  D.value = e instanceof Error ? e.message : String(e), E.value = "error";
94
96
  }
95
97
  }
96
98
  async function Q(t) {
97
- if (!F) return;
98
- let n = F, r = "";
99
- z = O.value?.scrollTop ?? 0, Y(!1);
100
- let i = ++L;
101
- H.value = t, W.value = "loading", G.value = "";
99
+ if (!I) return;
100
+ let n = I, r = "";
101
+ B = O.value?.scrollTop ?? 0, Y(!1);
102
+ let i = ++R;
103
+ U.value = t, G.value = "loading", K.value = "";
102
104
  try {
103
105
  let a = await o(n, t);
104
- if (L !== i || F !== n) return;
106
+ if (R !== i || I !== n) return;
105
107
  let s = URL.createObjectURL(new Blob([a], { type: t.contentType }));
106
108
  r = s;
107
109
  let c = e({
@@ -110,48 +112,48 @@ var ce = { class: "dx" }, le = { class: "dx__stage" }, x = {
110
112
  size: t.size,
111
113
  mime: t.contentType
112
114
  });
113
- if (L !== i || H.value !== t) {
115
+ if (R !== i || U.value !== t) {
114
116
  URL.revokeObjectURL(s), r = "";
115
117
  return;
116
118
  }
117
- I = s, r = "", U.value = c, W.value = "ready";
119
+ L = s, r = "", W.value = c, G.value = "ready";
118
120
  } catch (e) {
119
- if (r && URL.revokeObjectURL(r), L !== i) return;
120
- G.value = e instanceof Error ? e.message : String(e), W.value = "error";
121
+ if (r && URL.revokeObjectURL(r), R !== i) return;
122
+ K.value = e instanceof Error ? e.message : String(e), G.value = "error";
121
123
  }
122
124
  }
123
125
  async function $(e) {
124
- if (!F || K.value) return;
125
- let t = F;
126
- K.value = e.id, q.value = "";
126
+ if (!I || q.value) return;
127
+ let t = I;
128
+ q.value = e.id, J.value = "";
127
129
  let n = "";
128
130
  try {
129
131
  let r = await o(t, e);
130
- if (F !== t) return;
132
+ if (I !== t) return;
131
133
  n = URL.createObjectURL(new Blob([r], { type: e.contentType }));
132
134
  let i = document.createElement("a");
133
135
  i.href = n, i.download = e.name, document.body.appendChild(i), i.click(), i.remove();
134
136
  } catch (e) {
135
- q.value = e instanceof Error ? e.message : String(e);
137
+ J.value = e instanceof Error ? e.message : String(e);
136
138
  } finally {
137
- n && setTimeout(() => URL.revokeObjectURL(n), 1e3), K.value === e.id && (K.value = "");
139
+ n && setTimeout(() => URL.revokeObjectURL(n), 1e3), q.value === e.id && (q.value = "");
138
140
  }
139
141
  }
140
- return ae([() => T.url, O], ([e, t]) => {
142
+ return x([() => T.url, O], ([e, t]) => {
141
143
  e && t && Z();
142
- }, { immediate: !0 }), re(() => {
143
- R++, X(), O.value && (O.value.innerHTML = "");
144
- }), (e, a) => (_(), f("div", ce, [E.value === "ready" && !H.value ? (_(), u(i, {
144
+ }, { immediate: !0 }), ie(() => {
145
+ z++, X(), O.value && (O.value.innerHTML = "");
146
+ }), (e, a) => (_(), p("div", oe, [E.value === "ready" && !U.value ? (_(), d(i, {
145
147
  key: 0,
146
148
  collapsed: A.value,
147
149
  "onUpdate:collapsed": a[0] ||= (e) => A.value = e,
148
150
  view: k.value,
149
151
  "onUpdate:view": a[1] ||= (e) => k.value = e,
150
- "outline-items": b(me),
151
- "page-items": b(he),
152
- "active-outline-id": b(j),
153
- "active-page-id": b(M),
154
- onNavigate: b(P)
152
+ "outline-items": b(he),
153
+ "page-items": b(j),
154
+ "active-outline-id": b(M),
155
+ "active-page-id": b(N),
156
+ onNavigate: b(F)
155
157
  }, null, 8, [
156
158
  "collapsed",
157
159
  "view",
@@ -160,61 +162,61 @@ var ce = { class: "dx" }, le = { class: "dx__stage" }, x = {
160
162
  "active-outline-id",
161
163
  "active-page-id",
162
164
  "onNavigate"
163
- ])) : d("", !0), p("div", le, [
164
- H.value ? (_(), f("div", x, [
165
- p("button", {
165
+ ])) : f("", !0), m("div", se, [
166
+ U.value ? (_(), p("div", ce, [
167
+ m("button", {
166
168
  type: "button",
167
169
  class: "dx__back",
168
170
  title: "返回 Word 文档",
169
171
  onClick: a[2] ||= (e) => Y()
170
- }, [m(n, {
172
+ }, [h(n, {
171
173
  name: "arrowLeft",
172
174
  size: 16
173
175
  })]),
174
- p("span", {
176
+ m("span", {
175
177
  class: "dx__embedded-title",
176
- title: H.value.name
177
- }, y(H.value.label || H.value.name), 9, S),
178
- p("span", C, y(b(t)(H.value.size)), 1),
179
- p("button", {
178
+ title: U.value.name
179
+ }, y(U.value.label || U.value.name), 9, le),
180
+ m("span", ue, y(b(t)(U.value.size)), 1),
181
+ m("button", {
180
182
  type: "button",
181
183
  class: "dx__download",
182
184
  title: "下载内嵌文件",
183
- onClick: a[3] ||= (e) => $(H.value)
184
- }, [m(n, {
185
+ onClick: a[3] ||= (e) => $(U.value)
186
+ }, [h(n, {
185
187
  name: "download",
186
188
  size: 15
187
189
  })])
188
- ])) : d("", !0),
189
- E.value === "loading" ? (_(), f("p", ue, "正在渲染 Word 文档…")) : E.value === "error" ? (_(), f("div", de, [p("p", null, "渲染失败:" + y(D.value), 1), p("button", {
190
+ ])) : f("", !0),
191
+ E.value === "loading" ? (_(), p("p", de, "正在渲染 Word 文档…")) : E.value === "error" ? (_(), p("div", fe, [m("p", null, "渲染失败:" + y(D.value), 1), m("button", {
190
192
  type: "button",
191
193
  class: "dx__retry",
192
194
  onClick: Z
193
- }, "重试")])) : H.value ? (_(), f(l, { key: 3 }, [W.value === "loading" ? (_(), f("p", fe, "正在读取内嵌文件…")) : W.value === "error" ? (_(), f("div", pe, [p("p", null, "读取失败:" + y(G.value), 1), p("button", {
195
+ }, "重试")])) : U.value ? (_(), p(u, { key: 3 }, [G.value === "loading" ? (_(), p("p", pe, "正在读取内嵌文件…")) : G.value === "error" ? (_(), p("div", me, [m("p", null, "读取失败:" + y(K.value), 1), m("button", {
194
196
  type: "button",
195
197
  class: "dx__retry",
196
- onClick: a[4] ||= (e) => Q(H.value)
197
- }, " 重试 ")])) : U.value ? (_(), u(b(w), {
198
+ onClick: a[4] ||= (e) => Q(U.value)
199
+ }, " 重试 ")])) : W.value ? (_(), d(b(w), {
198
200
  key: 2,
199
- file: U.value,
201
+ file: W.value,
200
202
  mode: "render"
201
- }, null, 8, ["file"])) : d("", !0)], 64)) : E.value === "ready" && V.value.length ? (_(), u(r, {
203
+ }, null, 8, ["file"])) : f("", !0)], 64)) : E.value === "ready" && H.value.length ? (_(), d(r, {
202
204
  key: 4,
203
- files: V.value,
204
- "download-busy-id": K.value,
205
- error: q.value,
205
+ files: H.value,
206
+ "download-busy-id": q.value,
207
+ error: J.value,
206
208
  onOpen: Q,
207
209
  onDownload: $
208
210
  }, null, 8, [
209
211
  "files",
210
212
  "download-busy-id",
211
213
  "error"
212
- ])) : d("", !0),
213
- oe(p("div", {
214
+ ])) : f("", !0),
215
+ S(m("div", {
214
216
  ref_key: "docEl",
215
217
  ref: O,
216
218
  class: "dx__doc"
217
- }, null, 512), [[ie, E.value === "ready" && !H.value]])
219
+ }, null, 512), [[ae, E.value === "ready" && !U.value]])
218
220
  ])]));
219
221
  }
220
222
  });
@@ -1 +1 @@
1
- {"version":3,"file":"DocxViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DocxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { defineAsyncComponent, nextTick, onBeforeUnmount, shallowRef, watch } from 'vue'\nimport { renderAsync } from 'docx-preview'\nimport DocxEmbeddedFiles from '@/components/DocxEmbeddedFiles.vue'\nimport DocxNavigationPane from '@/components/DocxNavigationPane.vue'\nimport Icon from '@/components/icons/Icon.vue'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useElementNavigation } from '@/composables/useElementNavigation'\nimport {\n extractDocxEmbeddedFile,\n readDocxEmbeddedFiles,\n type DocxEmbeddedFile,\n} from '@/lib/docxEmbeddings'\nimport { mountDocxEmbeddedPreviews, type DocxEmbeddedDomController } from '@/lib/docxEmbeddedDom'\nimport {\n buildDocxNavigation,\n buildDocxPageNavigation,\n type ParsedDocxModel,\n} from '@/lib/docxNavigation'\nimport { formatBytes } from '@/lib/format'\nimport { classifyViewerFile } from '@/lib/viewerKind'\n\n// The shared body lets an extracted package reuse the same viewer as a top-level file. Lazy loading breaks the\n// FileViewerBody → DocxViewer → FileViewerBody module cycle, matching ZipBrowser's nested-entry hand-off.\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the\n * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a\n * paginated, paper-like preview (white pages on a neutral canvas, like Word). No CDN / network beyond the\n * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.\n *\n * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes. docx-preview injects its own CSS\n * into the render container, prefixed with the `className` ('docx') so it can't leak onto the page.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .docx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// The element docx-preview renders into. Always mounted (v-show, not v-if) so the ref exists when we render.\nconst docEl = shallowRef<HTMLElement | null>(null)\nconst navView = shallowRef<'outline' | 'pages'>('outline')\nconst navCollapsed = shallowRef(false)\nconst { outlineItems, pageItems, activeOutlineId, activePageId, setItems, navigate } =\n useElementNavigation(docEl)\n\nlet documentBuffer: ArrayBuffer | null = null\nlet embeddedBlobUrl = ''\nlet embeddedSelectionRun = 0\nlet renderRun = 0\nlet documentScrollTop = 0\nlet inlineEmbedded: DocxEmbeddedDomController | null = null\nconst embeddedFiles = shallowRef<DocxEmbeddedFile[]>([])\nconst selectedEmbedded = shallowRef<DocxEmbeddedFile | null>(null)\nconst embeddedViewerFile = shallowRef<ViewerFile | null>(null)\nconst embeddedStatus = shallowRef<Status>('ready')\nconst embeddedError = shallowRef('')\nconst downloadBusyId = shallowRef('')\nconst downloadError = shallowRef('')\n\nfunction revokeEmbeddedBlob(): void {\n if (!embeddedBlobUrl) return\n URL.revokeObjectURL(embeddedBlobUrl)\n embeddedBlobUrl = ''\n}\n\nfunction clearInlineEmbedded(): void {\n inlineEmbedded?.dispose()\n inlineEmbedded = null\n}\n\nfunction closeEmbedded(restoreDocumentScroll = true): void {\n const shouldRestoreScroll = restoreDocumentScroll && selectedEmbedded.value !== null\n embeddedSelectionRun++\n selectedEmbedded.value = null\n embeddedViewerFile.value = null\n embeddedStatus.value = 'ready'\n embeddedError.value = ''\n revokeEmbeddedBlob()\n if (shouldRestoreScroll) {\n void nextTick(() => {\n if (!selectedEmbedded.value && docEl.value) docEl.value.scrollTop = documentScrollTop\n })\n }\n}\n\nfunction resetEmbedded(): void {\n closeEmbedded(false)\n clearInlineEmbedded()\n documentBuffer = null\n documentScrollTop = 0\n embeddedFiles.value = []\n downloadBusyId.value = ''\n downloadError.value = ''\n}\n\nasync function render(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n const run = ++renderRun\n status.value = 'loading'\n errorMsg.value = ''\n resetEmbedded()\n setItems([], [])\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (renderRun !== run || props.url !== url || docEl.value !== el) return\n // Embedded packages are a best-effort enhancement: malformed relationship XML must not blank a Word file\n // that docx-preview can otherwise render. Parse concurrently because both consumers only read the buffer.\n const embeddedPromise = readDocxEmbeddedFiles(buf).catch((error) => {\n console.warn('DOCX embedded files unavailable:', error)\n return []\n })\n const document = (await renderAsync(buf, el, undefined, {\n className: 'docx', // CSS prefix → docx-preview's injected styles stay scoped to this subtree\n inWrapper: true, // wrap pages in the gray \"paper on desk\" canvas, like Word\n // The side panel is narrow (and resizable), so don't pin pages to the fixed A4 width — that overflows\n // and clips the left edge. Let pages fill the available width and reflow; height follows content.\n ignoreWidth: true,\n ignoreHeight: true,\n breakPages: true, // honor page breaks → still separate page sheets, stacked vertically\n renderHeaders: true,\n renderFooters: true,\n renderFootnotes: true,\n renderEndnotes: true,\n useBase64URL: true, // inline embedded images as data URLs (no blob-URL lifecycle to manage)\n })) as ParsedDocxModel\n const discoveredEmbeddedFiles = await embeddedPromise\n if (renderRun !== run || props.url !== url || docEl.value !== el) return\n let navigation\n try {\n navigation = buildDocxNavigation(document, el)\n } catch (error) {\n // A malformed/unsupported Word model must never turn an otherwise rendered document into an error page.\n console.warn('DOCX outline navigation unavailable:', error)\n navigation = { outline: [], pages: buildDocxPageNavigation(el) }\n }\n setItems(navigation.outline, navigation.pages)\n navView.value = navigation.outline.length ? 'outline' : 'pages'\n documentBuffer = buf\n embeddedFiles.value = discoveredEmbeddedFiles\n inlineEmbedded = mountDocxEmbeddedPreviews({\n root: el,\n buffer: buf,\n files: discoveredEmbeddedFiles,\n onOpen: (file) => void openEmbedded(file),\n })\n status.value = 'ready'\n } catch (e) {\n if (renderRun !== run) return\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nasync function openEmbedded(file: DocxEmbeddedFile): Promise<void> {\n if (!documentBuffer) return\n const buffer = documentBuffer\n let pendingUrl = ''\n documentScrollTop = docEl.value?.scrollTop ?? 0\n closeEmbedded(false)\n // closeEmbedded invalidates prior work; this run now becomes the active selection.\n const activeRun = ++embeddedSelectionRun\n selectedEmbedded.value = file\n embeddedStatus.value = 'loading'\n embeddedError.value = ''\n try {\n const bytes = await extractDocxEmbeddedFile(buffer, file)\n if (embeddedSelectionRun !== activeRun || documentBuffer !== buffer) return\n const url = URL.createObjectURL(new Blob([bytes], { type: file.contentType }))\n pendingUrl = url\n const viewerFile = classifyViewerFile({\n name: file.name,\n url,\n size: file.size,\n mime: file.contentType,\n })\n if (embeddedSelectionRun !== activeRun || selectedEmbedded.value !== file) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n embeddedBlobUrl = url\n pendingUrl = ''\n embeddedViewerFile.value = viewerFile\n embeddedStatus.value = 'ready'\n } catch (error) {\n if (pendingUrl) URL.revokeObjectURL(pendingUrl)\n if (embeddedSelectionRun !== activeRun) return\n embeddedError.value = error instanceof Error ? error.message : String(error)\n embeddedStatus.value = 'error'\n }\n}\n\nasync function downloadEmbedded(file: DocxEmbeddedFile): Promise<void> {\n if (!documentBuffer || downloadBusyId.value) return\n const buffer = documentBuffer\n downloadBusyId.value = file.id\n downloadError.value = ''\n let url = ''\n try {\n const bytes = await extractDocxEmbeddedFile(buffer, file)\n if (documentBuffer !== buffer) return\n url = URL.createObjectURL(new Blob([bytes], { type: file.contentType }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = file.name\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n } catch (error) {\n downloadError.value = error instanceof Error ? error.message : String(error)\n } finally {\n if (url) setTimeout(() => URL.revokeObjectURL(url), 1000)\n if (downloadBusyId.value === file.id) downloadBusyId.value = ''\n }\n}\n\nwatch(\n [() => props.url, docEl],\n ([url, element]) => {\n if (url && element) void render()\n },\n { immediate: true },\n)\nonBeforeUnmount(() => {\n renderRun++\n resetEmbedded()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"dx\">\n <DocxNavigationPane\n v-if=\"status === 'ready' && !selectedEmbedded\"\n v-model:collapsed=\"navCollapsed\"\n v-model:view=\"navView\"\n :outline-items=\"outlineItems\"\n :page-items=\"pageItems\"\n :active-outline-id=\"activeOutlineId\"\n :active-page-id=\"activePageId\"\n @navigate=\"navigate\"\n />\n <div class=\"dx__stage\">\n <div v-if=\"selectedEmbedded\" class=\"dx__embedded-bar\">\n <button type=\"button\" class=\"dx__back\" title=\"返回 Word 文档\" @click=\"closeEmbedded()\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"dx__embedded-title\" :title=\"selectedEmbedded.name\">\n {{ selectedEmbedded.label || selectedEmbedded.name }}\n </span>\n <span class=\"dx__embedded-size\">{{ formatBytes(selectedEmbedded.size) }}</span>\n <button\n type=\"button\"\n class=\"dx__download\"\n title=\"下载内嵌文件\"\n @click=\"downloadEmbedded(selectedEmbedded)\"\n >\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"status === 'loading'\" class=\"dx__msg\">正在渲染 Word 文档…</p>\n <div v-else-if=\"status === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"render\">重试</button>\n </div>\n <template v-else-if=\"selectedEmbedded\">\n <p v-if=\"embeddedStatus === 'loading'\" class=\"dx__msg\">正在读取内嵌文件…</p>\n <div v-else-if=\"embeddedStatus === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>读取失败:{{ embeddedError }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"openEmbedded(selectedEmbedded)\">\n 重试\n </button>\n </div>\n <FileViewerBody v-else-if=\"embeddedViewerFile\" :file=\"embeddedViewerFile\" mode=\"render\" />\n </template>\n\n <DocxEmbeddedFiles\n v-else-if=\"status === 'ready' && embeddedFiles.length\"\n :files=\"embeddedFiles\"\n :download-busy-id=\"downloadBusyId\"\n :error=\"downloadError\"\n @open=\"openEmbedded\"\n @download=\"downloadEmbedded\"\n />\n <!-- Always mounted (v-show) so docEl is available to docx-preview; hidden until render completes. -->\n <div v-show=\"status === 'ready' && !selectedEmbedded\" ref=\"docEl\" class=\"dx__doc\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.dx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.dx__stage {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.dx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dx__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dx__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.dx__embedded-bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.dx__back,\n.dx__download {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n padding: 0;\n color: var(--muted);\n background: transparent;\n border: none;\n border-radius: 7px;\n cursor: pointer;\n}\n.dx__back:hover,\n.dx__download:hover {\n color: var(--accent);\n background: var(--surface-2);\n}\n.dx__embedded-title {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 500;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.dx__embedded-size {\n flex-shrink: 0;\n color: var(--faint);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n/* docx-preview intentionally skips OLE `w:object` nodes but leaves their empty <p>. The DOM adapter inserts\n * Word/WPS's inert raster snapshot, or a rendered PPTX first slide when the snapshot is browser-incompatible,\n * into that exact paragraph. The whole preview opens the real embedded file. */\n.dx__doc :deep(.docx-embedded-host) {\n min-height: 0;\n margin: 8px 0 12px;\n text-align: center;\n}\n.dx__doc :deep(.docx-embedded-preview) {\n position: relative;\n display: inline-flex;\n box-sizing: border-box;\n align-items: center;\n justify-content: center;\n gap: 10px;\n width: 100%;\n min-height: 72px;\n padding: 12px;\n overflow: hidden;\n color: #334155;\n background: #f8fafc;\n border: 1px solid #cbd5e1;\n border-radius: 6px;\n box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);\n cursor: pointer;\n font-family: Arial, sans-serif;\n text-align: left;\n}\n.dx__doc :deep(.docx-embedded-preview:hover),\n.dx__doc :deep(.docx-embedded-preview:focus-visible) {\n border-color: #3370ff;\n box-shadow:\n 0 0 0 2px rgba(51, 112, 255, 0.16),\n 0 2px 8px rgba(15, 23, 42, 0.16);\n outline: none;\n}\n.dx__doc :deep(.docx-embedded-preview--image) {\n display: inline-block;\n min-height: 0;\n padding: 0;\n background: #fff;\n}\n.dx__doc :deep(.docx-embedded-preview--pptx) {\n display: block;\n min-height: 0;\n padding: 0;\n background: #fff;\n}\n.dx__doc :deep(.docx-embedded-preview--loading) {\n cursor: progress;\n}\n.dx__doc :deep(.docx-embedded-preview__pptx-slide) {\n width: 100%;\n height: 100%;\n overflow: hidden;\n pointer-events: none;\n}\n.dx__doc :deep(.docx-embedded-preview__pptx-status) {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #64748b;\n background: #f8fafc;\n font-size: 12px;\n font-weight: 600;\n}\n.dx__doc :deep(.docx-embedded-preview__image) {\n display: block;\n width: 100%;\n height: auto;\n object-fit: contain;\n}\n.dx__doc :deep(.docx-embedded-preview__hint) {\n position: absolute;\n right: 8px;\n bottom: 8px;\n padding: 5px 9px;\n color: #fff;\n background: rgba(15, 23, 42, 0.76);\n border-radius: 999px;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.02em;\n opacity: 0;\n pointer-events: none;\n transform: translateY(3px);\n transition:\n opacity 0.15s,\n transform 0.15s;\n}\n.dx__doc :deep(.docx-embedded-preview:hover .docx-embedded-preview__hint),\n.dx__doc :deep(.docx-embedded-preview:focus-visible .docx-embedded-preview__hint) {\n opacity: 1;\n transform: translateY(0);\n}\n.dx__doc :deep(.docx-embedded-preview__badge) {\n flex-shrink: 0;\n padding: 5px 7px;\n color: #166534;\n background: #dcfce7;\n border-radius: 5px;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.04em;\n}\n.dx__doc :deep(.docx-embedded-preview__fallback-label) {\n min-width: 0;\n overflow: hidden;\n font-size: 12px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Scroll region holding the rendered document. The pages keep their native Word appearance (white paper,\n * black text) for fidelity; we only retint the surrounding \"desk\" to the app's neutral surface so it sits\n * in the panel naturally in both light and dark themes. :deep reaches docx-preview's injected DOM. */\n.dx__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.dx__doc :deep(.docx-wrapper) {\n background: var(--surface-2);\n padding: 22px 16px 40px;\n gap: 16px;\n}\n/* Each page: a white sheet with a soft shadow. With ignoreWidth the page reflows to the panel width, so\n * pin it to fill the wrapper (box-sizing so the doc's own margins stay inside the sheet, not overflowing). */\n.dx__doc :deep(.docx-wrapper > section.docx) {\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n box-shadow: 0 2px 14px rgba(0, 0, 0, 0.18);\n margin-bottom: 0;\n}\n/* Some Word docs anchor tables as \"floating\" objects (OOXML `w:tblpPr` — a table pinned to a fixed spot on\n * the page with body text wrapping around it). docx-preview approximates that with `float: left` + a fixed\n * point width. On a full A4 page the table lands where Word put it, but in this narrow, width-reflowing\n * panel the floats stack unpredictably and the surrounding paragraphs wrap into the gaps beside them — so on\n * resize the layout scrambles and dark cell shading appears to streak across (the reported \"背景乱串 / 排版\n * 混乱\"). Since we've already chosen reflow over pixel-exact A4 fidelity (ignoreWidth + section width:100%),\n * force every table back to a normal full-width block that stacks top-to-bottom at any panel width, and let\n * long cell content wrap instead of overflowing. */\n.dx__doc :deep(section.docx table) {\n float: none !important;\n width: 100% !important;\n max-width: 100%;\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n.dx__doc :deep(section.docx td),\n.dx__doc :deep(section.docx th) {\n overflow-wrap: break-word;\n}\n/* The page's own side margins (this doc uses ~90pt) would swallow most of a narrow panel now that tables\n * fill the width, so cap the horizontal padding while keeping the top/bottom page margin intact. */\n.dx__doc :deep(section.docx) {\n padding-left: clamp(14px, 5%, 90pt) !important;\n padding-right: clamp(14px, 5%, 90pt) !important;\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"DocxViewer.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../src/components/DocxViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { defineAsyncComponent, nextTick, onBeforeUnmount, shallowRef, watch } from 'vue'\nimport { renderAsync } from 'docx-preview'\nimport DocxEmbeddedFiles from '@/components/DocxEmbeddedFiles.vue'\nimport DocxNavigationPane from '@/components/DocxNavigationPane.vue'\nimport Icon from '@/components/icons/Icon.vue'\nimport type { ViewerFile } from '@/composables/useFileViewer'\nimport { useElementNavigation } from '@/composables/useElementNavigation'\nimport {\n extractDocxEmbeddedFile,\n readDocxEmbeddedFiles,\n type DocxEmbeddedFile,\n} from '@/lib/docxEmbeddings'\nimport { mountDocxEmbeddedPreviews, type DocxEmbeddedDomController } from '@/lib/docxEmbeddedDom'\nimport {\n buildDocxNavigation,\n buildDocxPageNavigation,\n type ParsedDocxModel,\n} from '@/lib/docxNavigation'\nimport { formatBytes } from '@/lib/format'\nimport { applyDocxReadingTheme } from '@/lib/docxTheme'\nimport { classifyViewerFile } from '@/lib/viewerKind'\n\n// The shared body lets an extracted package reuse the same viewer as a top-level file. Lazy loading breaks the\n// FileViewerBody → DocxViewer → FileViewerBody module cycle, matching ZipBrowser's nested-entry hand-off.\nconst FileViewerBody = defineAsyncComponent(() => import('@/components/FileViewerBody.vue'))\n\n/**\n * Side-viewer Word (`.docx`) renderer. Fetches the document bytes from the download URL and renders the\n * OOXML to HTML entirely in-browser via docx-preview (https://github.com/VolodymyrBaydalka/docxjs) — a\n * paginated preview (plain pages follow the viewer; authored colors keep their paper). No CDN / network beyond the\n * file fetch: docx-preview bundles its own jszip + renderer, and images are inlined as base64.\n *\n * Two entry shapes mirror the other browsers (DICOM/zip): a real download URL for a held attachment, or a\n * blob URL minted by the archive browsers from already-extracted bytes. docx-preview injects its own CSS\n * into the render container, prefixed with the `className` ('docx') so it can't leak onto the page.\n */\nconst props = defineProps<{\n /** Download/blob URL of the .docx bytes. */\n url?: string\n name: string\n size?: number\n}>()\n\ntype Status = 'loading' | 'ready' | 'error'\nconst status = shallowRef<Status>('loading')\nconst errorMsg = shallowRef('')\n// The element docx-preview renders into. Always mounted (v-show, not v-if) so the ref exists when we render.\nconst docEl = shallowRef<HTMLElement | null>(null)\nconst navView = shallowRef<'outline' | 'pages'>('outline')\nconst navCollapsed = shallowRef(false)\nconst { outlineItems, pageItems, activeOutlineId, activePageId, setItems, navigate } =\n useElementNavigation(docEl)\n\nlet documentBuffer: ArrayBuffer | null = null\nlet embeddedBlobUrl = ''\nlet embeddedSelectionRun = 0\nlet renderRun = 0\nlet documentScrollTop = 0\nlet inlineEmbedded: DocxEmbeddedDomController | null = null\nconst embeddedFiles = shallowRef<DocxEmbeddedFile[]>([])\nconst selectedEmbedded = shallowRef<DocxEmbeddedFile | null>(null)\nconst embeddedViewerFile = shallowRef<ViewerFile | null>(null)\nconst embeddedStatus = shallowRef<Status>('ready')\nconst embeddedError = shallowRef('')\nconst downloadBusyId = shallowRef('')\nconst downloadError = shallowRef('')\n\nfunction revokeEmbeddedBlob(): void {\n if (!embeddedBlobUrl) return\n URL.revokeObjectURL(embeddedBlobUrl)\n embeddedBlobUrl = ''\n}\n\nfunction clearInlineEmbedded(): void {\n inlineEmbedded?.dispose()\n inlineEmbedded = null\n}\n\nfunction closeEmbedded(restoreDocumentScroll = true): void {\n const shouldRestoreScroll = restoreDocumentScroll && selectedEmbedded.value !== null\n embeddedSelectionRun++\n selectedEmbedded.value = null\n embeddedViewerFile.value = null\n embeddedStatus.value = 'ready'\n embeddedError.value = ''\n revokeEmbeddedBlob()\n if (shouldRestoreScroll) {\n void nextTick(() => {\n if (!selectedEmbedded.value && docEl.value) docEl.value.scrollTop = documentScrollTop\n })\n }\n}\n\nfunction resetEmbedded(): void {\n closeEmbedded(false)\n clearInlineEmbedded()\n documentBuffer = null\n documentScrollTop = 0\n embeddedFiles.value = []\n downloadBusyId.value = ''\n downloadError.value = ''\n}\n\nasync function render(): Promise<void> {\n const el = docEl.value\n if (!props.url || !el) return\n const run = ++renderRun\n status.value = 'loading'\n errorMsg.value = ''\n resetEmbedded()\n setItems([], [])\n el.innerHTML = '' // drop any prior render (e.g. on retry / switching files)\n const url = props.url\n try {\n const res = await fetch(url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n // Stale guard: a newer render may have started while we awaited the fetch.\n if (renderRun !== run || props.url !== url || docEl.value !== el) return\n // Embedded packages are a best-effort enhancement: malformed relationship XML must not blank a Word file\n // that docx-preview can otherwise render. Parse concurrently because both consumers only read the buffer.\n const embeddedPromise = readDocxEmbeddedFiles(buf).catch((error) => {\n console.warn('DOCX embedded files unavailable:', error)\n return []\n })\n const document = (await renderAsync(buf, el, undefined, {\n className: 'docx', // CSS prefix → docx-preview's injected styles stay scoped to this subtree\n inWrapper: true, // wrap pages in the gray \"paper on desk\" canvas, like Word\n // The side panel is narrow (and resizable), so don't pin pages to the fixed A4 width — that overflows\n // and clips the left edge. Let pages fill the available width and reflow; height follows content.\n ignoreWidth: true,\n ignoreHeight: true,\n breakPages: true, // honor page breaks → still separate page sheets, stacked vertically\n renderHeaders: true,\n renderFooters: true,\n renderFootnotes: true,\n renderEndnotes: true,\n useBase64URL: true, // inline embedded images as data URLs (no blob-URL lifecycle to manage)\n })) as ParsedDocxModel\n const discoveredEmbeddedFiles = await embeddedPromise\n if (renderRun !== run || props.url !== url || docEl.value !== el) return\n applyDocxReadingTheme(el)\n let navigation\n try {\n navigation = buildDocxNavigation(document, el)\n } catch (error) {\n // A malformed/unsupported Word model must never turn an otherwise rendered document into an error page.\n console.warn('DOCX outline navigation unavailable:', error)\n navigation = { outline: [], pages: buildDocxPageNavigation(el) }\n }\n setItems(navigation.outline, navigation.pages)\n navView.value = navigation.outline.length ? 'outline' : 'pages'\n documentBuffer = buf\n embeddedFiles.value = discoveredEmbeddedFiles\n inlineEmbedded = mountDocxEmbeddedPreviews({\n root: el,\n buffer: buf,\n files: discoveredEmbeddedFiles,\n onOpen: (file) => void openEmbedded(file),\n })\n status.value = 'ready'\n } catch (e) {\n if (renderRun !== run) return\n errorMsg.value = e instanceof Error ? e.message : String(e)\n status.value = 'error'\n }\n}\n\nasync function openEmbedded(file: DocxEmbeddedFile): Promise<void> {\n if (!documentBuffer) return\n const buffer = documentBuffer\n let pendingUrl = ''\n documentScrollTop = docEl.value?.scrollTop ?? 0\n closeEmbedded(false)\n // closeEmbedded invalidates prior work; this run now becomes the active selection.\n const activeRun = ++embeddedSelectionRun\n selectedEmbedded.value = file\n embeddedStatus.value = 'loading'\n embeddedError.value = ''\n try {\n const bytes = await extractDocxEmbeddedFile(buffer, file)\n if (embeddedSelectionRun !== activeRun || documentBuffer !== buffer) return\n const url = URL.createObjectURL(new Blob([bytes], { type: file.contentType }))\n pendingUrl = url\n const viewerFile = classifyViewerFile({\n name: file.name,\n url,\n size: file.size,\n mime: file.contentType,\n })\n if (embeddedSelectionRun !== activeRun || selectedEmbedded.value !== file) {\n URL.revokeObjectURL(url)\n pendingUrl = ''\n return\n }\n embeddedBlobUrl = url\n pendingUrl = ''\n embeddedViewerFile.value = viewerFile\n embeddedStatus.value = 'ready'\n } catch (error) {\n if (pendingUrl) URL.revokeObjectURL(pendingUrl)\n if (embeddedSelectionRun !== activeRun) return\n embeddedError.value = error instanceof Error ? error.message : String(error)\n embeddedStatus.value = 'error'\n }\n}\n\nasync function downloadEmbedded(file: DocxEmbeddedFile): Promise<void> {\n if (!documentBuffer || downloadBusyId.value) return\n const buffer = documentBuffer\n downloadBusyId.value = file.id\n downloadError.value = ''\n let url = ''\n try {\n const bytes = await extractDocxEmbeddedFile(buffer, file)\n if (documentBuffer !== buffer) return\n url = URL.createObjectURL(new Blob([bytes], { type: file.contentType }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = file.name\n document.body.appendChild(anchor)\n anchor.click()\n anchor.remove()\n } catch (error) {\n downloadError.value = error instanceof Error ? error.message : String(error)\n } finally {\n if (url) setTimeout(() => URL.revokeObjectURL(url), 1000)\n if (downloadBusyId.value === file.id) downloadBusyId.value = ''\n }\n}\n\nwatch(\n [() => props.url, docEl],\n ([url, element]) => {\n if (url && element) void render()\n },\n { immediate: true },\n)\nonBeforeUnmount(() => {\n renderRun++\n resetEmbedded()\n if (docEl.value) docEl.value.innerHTML = ''\n})\n</script>\n\n<template>\n <div class=\"dx\">\n <DocxNavigationPane\n v-if=\"status === 'ready' && !selectedEmbedded\"\n v-model:collapsed=\"navCollapsed\"\n v-model:view=\"navView\"\n :outline-items=\"outlineItems\"\n :page-items=\"pageItems\"\n :active-outline-id=\"activeOutlineId\"\n :active-page-id=\"activePageId\"\n @navigate=\"navigate\"\n />\n <div class=\"dx__stage\">\n <div v-if=\"selectedEmbedded\" class=\"dx__embedded-bar\">\n <button type=\"button\" class=\"dx__back\" title=\"返回 Word 文档\" @click=\"closeEmbedded()\">\n <Icon name=\"arrowLeft\" :size=\"16\" />\n </button>\n <span class=\"dx__embedded-title\" :title=\"selectedEmbedded.name\">\n {{ selectedEmbedded.label || selectedEmbedded.name }}\n </span>\n <span class=\"dx__embedded-size\">{{ formatBytes(selectedEmbedded.size) }}</span>\n <button\n type=\"button\"\n class=\"dx__download\"\n title=\"下载内嵌文件\"\n @click=\"downloadEmbedded(selectedEmbedded)\"\n >\n <Icon name=\"download\" :size=\"15\" />\n </button>\n </div>\n\n <p v-if=\"status === 'loading'\" class=\"dx__msg\">正在渲染 Word 文档…</p>\n <div v-else-if=\"status === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>渲染失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"render\">重试</button>\n </div>\n <template v-else-if=\"selectedEmbedded\">\n <p v-if=\"embeddedStatus === 'loading'\" class=\"dx__msg\">正在读取内嵌文件…</p>\n <div v-else-if=\"embeddedStatus === 'error'\" class=\"dx__msg dx__msg--error\">\n <p>读取失败:{{ embeddedError }}</p>\n <button type=\"button\" class=\"dx__retry\" @click=\"openEmbedded(selectedEmbedded)\">\n 重试\n </button>\n </div>\n <FileViewerBody v-else-if=\"embeddedViewerFile\" :file=\"embeddedViewerFile\" mode=\"render\" />\n </template>\n\n <DocxEmbeddedFiles\n v-else-if=\"status === 'ready' && embeddedFiles.length\"\n :files=\"embeddedFiles\"\n :download-busy-id=\"downloadBusyId\"\n :error=\"downloadError\"\n @open=\"openEmbedded\"\n @download=\"downloadEmbedded\"\n />\n <!-- Always mounted (v-show) so docEl is available to docx-preview; hidden until render completes. -->\n <div v-show=\"status === 'ready' && !selectedEmbedded\" ref=\"docEl\" class=\"dx__doc\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.dx {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: row;\n background: var(--surface);\n container-type: inline-size;\n}\n.dx__stage {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: flex;\n flex-direction: column;\n}\n.dx__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.dx__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.dx__retry {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.dx__retry:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n.dx__embedded-bar {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n padding: 7px 10px 7px 8px;\n border-bottom: 1px solid var(--border);\n background: var(--surface);\n}\n.dx__back,\n.dx__download {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 30px;\n height: 28px;\n padding: 0;\n color: var(--muted);\n background: transparent;\n border: none;\n border-radius: 7px;\n cursor: pointer;\n}\n.dx__back:hover,\n.dx__download:hover {\n color: var(--accent);\n background: var(--surface-2);\n}\n.dx__embedded-title {\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n color: var(--text-strong);\n font-size: 12.5px;\n font-weight: 500;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.dx__embedded-size {\n flex-shrink: 0;\n color: var(--faint);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n}\n\n/* docx-preview intentionally skips OLE `w:object` nodes but leaves their empty <p>. The DOM adapter inserts\n * Word/WPS's inert raster snapshot, or a rendered PPTX first slide when the snapshot is browser-incompatible,\n * into that exact paragraph. The whole preview opens the real embedded file. */\n.dx__doc :deep(.docx-embedded-host) {\n min-height: 0;\n margin: 8px 0 12px;\n text-align: center;\n}\n.dx__doc :deep(.docx-embedded-preview) {\n position: relative;\n display: inline-flex;\n box-sizing: border-box;\n align-items: center;\n justify-content: center;\n gap: 10px;\n width: 100%;\n min-height: 72px;\n padding: 12px;\n overflow: hidden;\n color: var(--text);\n background: var(--surface-2);\n border: 1px solid var(--border-2);\n border-radius: 6px;\n box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);\n cursor: pointer;\n font-family: Arial, sans-serif;\n text-align: left;\n}\n.dx__doc :deep(.docx-embedded-preview:hover),\n.dx__doc :deep(.docx-embedded-preview:focus-visible) {\n border-color: var(--hl-attr);\n box-shadow:\n 0 0 0 2px color-mix(in srgb, var(--hl-attr) 16%, transparent),\n 0 2px 8px rgba(15, 23, 42, 0.16);\n outline: none;\n}\n.dx__doc :deep(.docx-embedded-preview--image) {\n display: inline-block;\n min-height: 0;\n padding: 0;\n background: #fff;\n}\n.dx__doc :deep(.docx-embedded-preview--pptx) {\n display: block;\n min-height: 0;\n padding: 0;\n background: #fff;\n}\n.dx__doc :deep(.docx-embedded-preview--loading) {\n cursor: progress;\n}\n.dx__doc :deep(.docx-embedded-preview__pptx-slide) {\n width: 100%;\n height: 100%;\n overflow: hidden;\n pointer-events: none;\n}\n.dx__doc :deep(.docx-embedded-preview__pptx-status) {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--muted);\n background: var(--surface-2);\n font-size: 12px;\n font-weight: 600;\n}\n.dx__doc :deep(.docx-embedded-preview__image) {\n display: block;\n width: 100%;\n height: auto;\n object-fit: contain;\n}\n.dx__doc :deep(.docx-embedded-preview__hint) {\n position: absolute;\n right: 8px;\n bottom: 8px;\n padding: 5px 9px;\n color: #fff;\n background: rgba(15, 23, 42, 0.76);\n border-radius: 999px;\n font-size: 10px;\n font-weight: 600;\n letter-spacing: 0.02em;\n opacity: 0;\n pointer-events: none;\n transform: translateY(3px);\n transition:\n opacity 0.15s,\n transform 0.15s;\n}\n.dx__doc :deep(.docx-embedded-preview:hover .docx-embedded-preview__hint),\n.dx__doc :deep(.docx-embedded-preview:focus-visible .docx-embedded-preview__hint) {\n opacity: 1;\n transform: translateY(0);\n}\n.dx__doc :deep(.docx-embedded-preview__badge) {\n flex-shrink: 0;\n padding: 5px 7px;\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 14%, var(--surface));\n border-radius: 5px;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.04em;\n}\n.dx__doc :deep(.docx-embedded-preview__fallback-label) {\n min-width: 0;\n overflow: hidden;\n font-size: 12px;\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Scroll region holding the rendered document. The pages keep their native Word appearance (white paper,\n * black text) for fidelity; we only retint the surrounding \"desk\" to the app's neutral surface so it sits\n * in the panel naturally in both light and dark themes. :deep reaches docx-preview's injected DOM. */\n.dx__doc {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--surface-2);\n}\n.dx__doc :deep(.docx-wrapper) {\n background: var(--surface-2);\n padding: 22px 16px 40px;\n gap: 16px;\n}\n/* Each page gets a soft shadow. With ignoreWidth the page reflows to the panel width, so\n * pin it to fill the wrapper (box-sizing so the doc's own margins stay inside the sheet, not overflowing). */\n.dx__doc :deep(.docx-wrapper > section.docx) {\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n box-shadow: var(--shadow);\n margin-bottom: 0;\n}\n/* Complex pages retain their original paper/native controls; simple pages inherit the viewer scheme. */\n.dx__doc :deep(section.docx:not([data-fv-reading-theme='adaptive'])) {\n color-scheme: light;\n}\n/* Some Word docs anchor tables as \"floating\" objects (OOXML `w:tblpPr` — a table pinned to a fixed spot on\n * the page with body text wrapping around it). docx-preview approximates that with `float: left` + a fixed\n * point width. On a full A4 page the table lands where Word put it, but in this narrow, width-reflowing\n * panel the floats stack unpredictably and the surrounding paragraphs wrap into the gaps beside them — so on\n * resize the layout scrambles and dark cell shading appears to streak across (the reported \"背景乱串 / 排版\n * 混乱\"). Since we've already chosen reflow over pixel-exact A4 fidelity (ignoreWidth + section width:100%),\n * force every table back to a normal full-width block that stacks top-to-bottom at any panel width, and let\n * long cell content wrap instead of overflowing. */\n.dx__doc :deep(section.docx table) {\n float: none !important;\n width: 100% !important;\n max-width: 100%;\n margin-left: 0 !important;\n margin-right: 0 !important;\n}\n.dx__doc :deep(section.docx td),\n.dx__doc :deep(section.docx th) {\n overflow-wrap: break-word;\n}\n/* The page's own side margins (this doc uses ~90pt) would swallow most of a narrow panel now that tables\n * fill the width, so cap the horizontal padding while keeping the top/bottom page margin intact. */\n.dx__doc :deep(section.docx) {\n padding-left: clamp(14px, 5%, 90pt) !important;\n padding-right: clamp(14px, 5%, 90pt) !important;\n}\n</style>\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import e from "../_virtual/_plugin-vue_export-helper.js";
2
2
  import t from "./EplViewer.vue_vue_type_script_setup_true_lang.js";
3
3
  /* empty css */
4
4
  //#region src/components/EplViewer.vue
5
- var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-6b3500dc"]]);
5
+ var n = /*#__PURE__*/ e(t, [["__scopeId", "data-v-562b3ee6"]]);
6
6
  //#endregion
7
7
  export { n as default };
8
8
 
@@ -1 +1 @@
1
- {"version":3,"file":"EplViewer.js","names":[],"sources":["../../src/components/EplViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { decompileEpl, EplEncryptedError, type EplPreview } from '@/lib/epl'\nimport EplTree from './epl/EplTree.vue'\nimport EplDoc from './epl/EplDoc.vue'\nimport type { EplDocRef, EplDocKind } from './epl/docTypes'\n\n/**\n * IDE-style viewer for e-language (易语言) `.e` / `.ec` project files. Fetches the BYTES and parses + decompiles\n * them entirely in-browser via {@link decompileEpl} (port of OpenEpl/EProjectFile, see lib/epl) — no dependency,\n * like the eml / mht / binobj viewers. Lays the result out like the 易语言 IDE: a「程序」tree on the left\n * (程序集 / 类 + project tables), the selected document on the right (definitions as bordered tables, method\n * bodies with dashed block guides), open documents as bottom tabs, and 支持库/程序 tabs under the tree.\n *\n * Encrypted files surface a friendly message (decryption is a separate path); a large file is gated behind a\n * confirm (all parsing is in-memory). Entry shape mirrors the other self-fetching viewers: a real download URL,\n * or a blob URL minted by an archive browser from extracted bytes.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error' | 'encrypted'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst encryptTip = ref('')\nconst preview = ref<EplPreview | null>(null)\n\nconst SIZE_LIMIT = 32 * 1024 * 1024\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n encryptTip.value = ''\n preview.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n preview.value = decompileEpl(new Uint8Array(buf))\n status.value = 'ready'\n } catch (e) {\n if (e instanceof EplEncryptedError) {\n status.value = 'encrypted'\n encryptTip.value = e.tip ?? ''\n } else {\n status.value = 'error'\n errorMsg.value = e instanceof Error ? e.message : String(e)\n }\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\n\nconst meta = computed(() => preview.value?.meta ?? null)\nconst program = computed(() => preview.value?.program ?? null)\nconst fileTypeLabel = computed(() => {\n const t = meta.value?.fileType\n return t === 'source' ? '源码 .e' : t === 'module' ? '模块 .ec' : '未知'\n})\n\n// ── IDE state: side tab + open document tabs ──────────────────────────────────────────────────────\nconst sideTab = ref<'program' | 'libs'>('program')\nconst openTabs = ref<EplDocRef[]>([])\nconst activeId = ref('')\nconst activeDoc = computed(() => openTabs.value.find((t) => t.id === activeId.value) ?? null)\n\nconst TABLE_TITLE: Record<Exclude<EplDocKind, 'class'>, string> = {\n globals: '全局变量',\n structs: '自定义数据类型',\n dll: 'Dll命令',\n constants: '常量表',\n resources: '图片/声音',\n forms: '资源表',\n libraries: '模块引用表',\n}\n\nfunction defaultDoc(p: NonNullable<typeof preview.value>['program']): EplDocRef | null {\n if (p.classes.length) {\n const c = p.classes[0]\n return { id: `class:${c.id >>> 0}`, kind: 'class', classId: c.id, title: c.name }\n }\n const order: Exclude<EplDocKind, 'class'>[] = [\n 'globals',\n 'structs',\n 'dll',\n 'constants',\n 'resources',\n 'forms',\n 'libraries',\n ]\n const counts: Record<string, number> = {\n globals: p.globals.length,\n structs: p.structs.length,\n dll: p.dllDeclares.length,\n constants: p.constants.length,\n resources: p.resources.length,\n forms: p.forms.length,\n libraries: p.libraries.length,\n }\n for (const k of order) {\n if (counts[k] > 0) return { id: k, kind: k, title: TABLE_TITLE[k] }\n }\n return null\n}\n\nwatch(preview, (p) => {\n openTabs.value = []\n activeId.value = ''\n sideTab.value = 'program'\n if (p) {\n const d = defaultDoc(p.program)\n if (d) openDoc(d)\n }\n})\n\nfunction openDoc(ref: EplDocRef): void {\n if (!openTabs.value.some((t) => t.id === ref.id)) openTabs.value = [...openTabs.value, ref]\n activeId.value = ref.id\n}\n\nfunction closeTab(id: string): void {\n const idx = openTabs.value.findIndex((t) => t.id === id)\n if (idx < 0) return\n const next = openTabs.value.filter((t) => t.id !== id)\n openTabs.value = next\n if (activeId.value === id) {\n const fallback = next[idx] ?? next[idx - 1] ?? null\n activeId.value = fallback ? fallback.id : ''\n }\n}\n\nasync function onSelectMethod(payload: { classId: number; methodId: number }): Promise<void> {\n const p = program.value\n if (!p) return\n const c = p.classes.find((x) => x.id === payload.classId)\n if (!c) return\n openDoc({\n id: `class:${payload.classId >>> 0}`,\n kind: 'class',\n classId: payload.classId,\n title: c.name,\n })\n await nextTick()\n document.getElementById(`epl-m-${payload.methodId >>> 0}`)?.scrollIntoView({ block: 'start' })\n}\n\nfunction openLibraries(): void {\n openDoc({ id: 'libraries', kind: 'libraries', title: '模块引用表' })\n}\n</script>\n\n<template>\n <div class=\"epl\">\n <p v-if=\"status === 'loading'\" class=\"epl__msg\">正在解析易语言工程…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"epl__msg epl__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'encrypted'\" class=\"epl__msg epl__gate\">\n <p>这是加密的易语言工程文件,暂未支持解密预览。</p>\n <p v-if=\"encryptTip\" class=\"epl__tip\">密码提示:{{ encryptTip }}</p>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"epl__msg epl__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"status === 'ready' && meta && program\" class=\"epl__ide\">\n <!-- top bar: project metadata -->\n <div class=\"epl__top\">\n <span class=\"epl__name\">{{ meta.project?.name || props.name }}</span>\n <span class=\"epl__badge\">{{ fileTypeLabel }}</span>\n <span class=\"epl__counts\">\n 程序集 {{ meta.counts.classes }} · 子程序 {{ meta.counts.methods }} · 数据类型\n {{ meta.counts.structs }} · DLL {{ meta.counts.dllDeclares }}\n <template v-if=\"meta.systemVersion\"> · 易语言 {{ meta.systemVersion }}</template>\n </span>\n </div>\n\n <div class=\"epl__body\">\n <!-- left: tree + side tabs -->\n <aside class=\"epl__side\">\n <div class=\"epl__sidebody\">\n <EplTree\n v-show=\"sideTab === 'program'\"\n :program=\"program\"\n :active-id=\"activeId\"\n @select=\"openDoc\"\n @select-method=\"onSelectMethod\"\n />\n <div v-show=\"sideTab === 'libs'\" class=\"epl__libs\">\n <div class=\"epl__libsroot\">支持库</div>\n <button\n v-for=\"(l, i) in program.libraries\"\n :key=\"i\"\n type=\"button\"\n class=\"epl__lib\"\n :title=\"l.fileName\"\n @click=\"openLibraries\"\n >\n <span class=\"epl__libico\">库</span>{{ l.name || l.fileName }}\n </button>\n <p v-if=\"!program.libraries.length\" class=\"epl__libsempty\">(无引用支持库)</p>\n </div>\n </div>\n <div class=\"epl__sidetabs\">\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'libs' }\"\n @click=\"sideTab = 'libs'\"\n >\n 支持库\n </button>\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'program' }\"\n @click=\"sideTab = 'program'\"\n >\n 程序\n </button>\n </div>\n </aside>\n\n <!-- right: document + bottom tabs -->\n <main class=\"epl__main\">\n <div class=\"epl__docarea\">\n <EplDoc v-if=\"activeDoc\" :program=\"program\" :doc=\"activeDoc\" />\n <p v-else class=\"epl__msg\">从左侧选择程序集、类或表进行查看。</p>\n </div>\n <div v-if=\"openTabs.length\" class=\"epl__doctabs\">\n <div\n v-for=\"t in openTabs\"\n :key=\"t.id\"\n class=\"epl__doctab\"\n :class=\"{ 'is-active': t.id === activeId }\"\n >\n <button type=\"button\" class=\"epl__doctab-label\" @click=\"activeId = t.id\">\n {{ t.title }}\n </button>\n <button type=\"button\" class=\"epl__doctab-close\" title=\"关闭\" @click=\"closeTab(t.id)\">\n ×\n </button>\n </div>\n </div>\n </main>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.epl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n\n /* Fixed light \"IDE\" palette so the viewer reads like 易语言 regardless of the app theme. */\n --epl-bg: #ffffff;\n --epl-fg: #1f1f1f;\n --epl-border: #c4ccc4;\n --epl-th-bg: #dce8dc;\n --epl-th-fg: #2c3a2c;\n --epl-ident: #1b50c8;\n --epl-keyword: #0a3fb0;\n --epl-string: #c01818;\n --epl-comment: #1f8a3b;\n --epl-check: #1a7a1a;\n --epl-guide: #9aa6b2;\n --epl-gutter: #97a0a8;\n --epl-gutter-bg: #f4f6f4;\n --epl-row-hover: #eef4ff;\n --epl-sel: #cfe0ff;\n --epl-side-bg: #f6f7f6;\n --epl-tabbar-bg: #dfe3df;\n --epl-tab-bg: #e9ece9;\n --epl-tab-active-bg: #ffffff;\n}\n\n/* ── status messages ─────────────────────────────────────────────────────── */\n.epl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.epl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.epl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.epl__tip {\n color: var(--faint);\n font-size: 12.5px;\n}\n.epl__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.epl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* ── IDE shell ───────────────────────────────────────────────────────────── */\n.epl__ide {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--epl-bg);\n color: var(--epl-fg);\n}\n.epl__top {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 6px 12px;\n border-bottom: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n font-size: 12.5px;\n}\n.epl__name {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__badge {\n font-size: 11px;\n color: #0a5;\n background: #e3f3e8;\n border-radius: 999px;\n padding: 1px 8px;\n}\n.epl__counts {\n color: #5a6a5a;\n}\n\n.epl__body {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* left side */\n.epl__side {\n flex: 0 0 auto;\n width: 248px;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n}\n.epl__sidebody {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.epl__sidetabs {\n flex: 0 0 auto;\n display: flex;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__sidetabs button {\n flex: 1 1 0;\n padding: 5px 8px;\n border: none;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n}\n.epl__sidetabs button:last-child {\n border-right: none;\n}\n.epl__sidetabs button.is-active {\n background: var(--epl-tab-active-bg);\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__libs {\n padding: 6px 0;\n font-size: 12.5px;\n}\n.epl__libsroot {\n padding: 2px 10px 4px;\n font-weight: 600;\n color: var(--epl-th-fg);\n}\n.epl__lib {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 2px 10px 2px 18px;\n border: none;\n background: none;\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: inherit;\n}\n.epl__lib:hover {\n color: var(--epl-ident);\n}\n.epl__libico {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n border-radius: 3px;\n font-size: 10px;\n color: #fff;\n background: #c08a4a;\n}\n.epl__libsempty {\n padding: 4px 12px;\n color: var(--epl-gutter);\n}\n\n/* right side */\n.epl__main {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.epl__docarea {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--epl-bg);\n}\n.epl__doctabs {\n flex: 0 0 auto;\n display: flex;\n overflow-x: auto;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__doctab {\n display: flex;\n align-items: center;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n white-space: nowrap;\n}\n.epl__doctab.is-active {\n background: var(--epl-tab-active-bg);\n}\n.epl__doctab-label {\n padding: 5px 6px 5px 12px;\n border: none;\n background: none;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: #444;\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.epl__doctab.is-active .epl__doctab-label {\n font-weight: 600;\n color: #1c1c1c;\n}\n.epl__doctab-close {\n padding: 0 8px 0 2px;\n border: none;\n background: none;\n cursor: pointer;\n color: #999;\n font-size: 14px;\n line-height: 1;\n}\n.epl__doctab-close:hover {\n color: var(--epl-string);\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"EplViewer.js","names":[],"sources":["../../src/components/EplViewer.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { decompileEpl, EplEncryptedError, type EplPreview } from '@/lib/epl'\nimport EplTree from './epl/EplTree.vue'\nimport EplDoc from './epl/EplDoc.vue'\nimport type { EplDocRef, EplDocKind } from './epl/docTypes'\n\n/**\n * IDE-style viewer for e-language (易语言) `.e` / `.ec` project files. Fetches the BYTES and parses + decompiles\n * them entirely in-browser via {@link decompileEpl} (port of OpenEpl/EProjectFile, see lib/epl) — no dependency,\n * like the eml / mht / binobj viewers. Lays the result out like the 易语言 IDE: a「程序」tree on the left\n * (程序集 / 类 + project tables), the selected document on the right (definitions as bordered tables, method\n * bodies with dashed block guides), open documents as bottom tabs, and 支持库/程序 tabs under the tree.\n *\n * Encrypted files surface a friendly message (decryption is a separate path); a large file is gated behind a\n * confirm (all parsing is in-memory). Entry shape mirrors the other self-fetching viewers: a real download URL,\n * or a blob URL minted by an archive browser from extracted bytes.\n */\nconst props = defineProps<{ url?: string; name: string; size?: number }>()\n\ntype Status = 'loading' | 'gate' | 'ready' | 'error' | 'encrypted'\nconst status = ref<Status>('loading')\nconst errorMsg = ref('')\nconst encryptTip = ref('')\nconst preview = ref<EplPreview | null>(null)\n\nconst SIZE_LIMIT = 32 * 1024 * 1024\n\nasync function doLoad(): Promise<void> {\n if (!props.url) return\n status.value = 'loading'\n errorMsg.value = ''\n encryptTip.value = ''\n preview.value = null\n try {\n const res = await fetch(props.url)\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n const buf = await res.arrayBuffer()\n preview.value = decompileEpl(new Uint8Array(buf))\n status.value = 'ready'\n } catch (e) {\n if (e instanceof EplEncryptedError) {\n status.value = 'encrypted'\n encryptTip.value = e.tip ?? ''\n } else {\n status.value = 'error'\n errorMsg.value = e instanceof Error ? e.message : String(e)\n }\n }\n}\n\nfunction load(): void {\n if (props.size && props.size > SIZE_LIMIT) {\n status.value = 'gate'\n return\n }\n void doLoad()\n}\n\nwatch(() => props.url, load, { immediate: true })\n\nconst meta = computed(() => preview.value?.meta ?? null)\nconst program = computed(() => preview.value?.program ?? null)\nconst fileTypeLabel = computed(() => {\n const t = meta.value?.fileType\n return t === 'source' ? '源码 .e' : t === 'module' ? '模块 .ec' : '未知'\n})\n\n// ── IDE state: side tab + open document tabs ──────────────────────────────────────────────────────\nconst sideTab = ref<'program' | 'libs'>('program')\nconst openTabs = ref<EplDocRef[]>([])\nconst activeId = ref('')\nconst activeDoc = computed(() => openTabs.value.find((t) => t.id === activeId.value) ?? null)\n\nconst TABLE_TITLE: Record<Exclude<EplDocKind, 'class'>, string> = {\n globals: '全局变量',\n structs: '自定义数据类型',\n dll: 'Dll命令',\n constants: '常量表',\n resources: '图片/声音',\n forms: '资源表',\n libraries: '模块引用表',\n}\n\nfunction defaultDoc(p: NonNullable<typeof preview.value>['program']): EplDocRef | null {\n if (p.classes.length) {\n const c = p.classes[0]\n return { id: `class:${c.id >>> 0}`, kind: 'class', classId: c.id, title: c.name }\n }\n const order: Exclude<EplDocKind, 'class'>[] = [\n 'globals',\n 'structs',\n 'dll',\n 'constants',\n 'resources',\n 'forms',\n 'libraries',\n ]\n const counts: Record<string, number> = {\n globals: p.globals.length,\n structs: p.structs.length,\n dll: p.dllDeclares.length,\n constants: p.constants.length,\n resources: p.resources.length,\n forms: p.forms.length,\n libraries: p.libraries.length,\n }\n for (const k of order) {\n if (counts[k] > 0) return { id: k, kind: k, title: TABLE_TITLE[k] }\n }\n return null\n}\n\nwatch(preview, (p) => {\n openTabs.value = []\n activeId.value = ''\n sideTab.value = 'program'\n if (p) {\n const d = defaultDoc(p.program)\n if (d) openDoc(d)\n }\n})\n\nfunction openDoc(ref: EplDocRef): void {\n if (!openTabs.value.some((t) => t.id === ref.id)) openTabs.value = [...openTabs.value, ref]\n activeId.value = ref.id\n}\n\nfunction closeTab(id: string): void {\n const idx = openTabs.value.findIndex((t) => t.id === id)\n if (idx < 0) return\n const next = openTabs.value.filter((t) => t.id !== id)\n openTabs.value = next\n if (activeId.value === id) {\n const fallback = next[idx] ?? next[idx - 1] ?? null\n activeId.value = fallback ? fallback.id : ''\n }\n}\n\nasync function onSelectMethod(payload: { classId: number; methodId: number }): Promise<void> {\n const p = program.value\n if (!p) return\n const c = p.classes.find((x) => x.id === payload.classId)\n if (!c) return\n openDoc({\n id: `class:${payload.classId >>> 0}`,\n kind: 'class',\n classId: payload.classId,\n title: c.name,\n })\n await nextTick()\n document.getElementById(`epl-m-${payload.methodId >>> 0}`)?.scrollIntoView({ block: 'start' })\n}\n\nfunction openLibraries(): void {\n openDoc({ id: 'libraries', kind: 'libraries', title: '模块引用表' })\n}\n</script>\n\n<template>\n <div class=\"epl\">\n <p v-if=\"status === 'loading'\" class=\"epl__msg\">正在解析易语言工程…</p>\n\n <div v-else-if=\"status === 'gate'\" class=\"epl__msg epl__gate\">\n <p>文件较大({{ ((props.size ?? 0) / 1024 / 1024).toFixed(1) }} MB),是否继续解析预览?</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">继续预览</button>\n </div>\n\n <div v-else-if=\"status === 'encrypted'\" class=\"epl__msg epl__gate\">\n <p>这是加密的易语言工程文件,暂未支持解密预览。</p>\n <p v-if=\"encryptTip\" class=\"epl__tip\">密码提示:{{ encryptTip }}</p>\n </div>\n\n <div v-else-if=\"status === 'error'\" class=\"epl__msg epl__msg--error\">\n <p>解析失败:{{ errorMsg }}</p>\n <button type=\"button\" class=\"epl__btn\" @click=\"doLoad\">重试</button>\n </div>\n\n <div v-else-if=\"status === 'ready' && meta && program\" class=\"epl__ide\">\n <!-- top bar: project metadata -->\n <div class=\"epl__top\">\n <span class=\"epl__name\">{{ meta.project?.name || props.name }}</span>\n <span class=\"epl__badge\">{{ fileTypeLabel }}</span>\n <span class=\"epl__counts\">\n 程序集 {{ meta.counts.classes }} · 子程序 {{ meta.counts.methods }} · 数据类型\n {{ meta.counts.structs }} · DLL {{ meta.counts.dllDeclares }}\n <template v-if=\"meta.systemVersion\"> · 易语言 {{ meta.systemVersion }}</template>\n </span>\n </div>\n\n <div class=\"epl__body\">\n <!-- left: tree + side tabs -->\n <aside class=\"epl__side\">\n <div class=\"epl__sidebody\">\n <EplTree\n v-show=\"sideTab === 'program'\"\n :program=\"program\"\n :active-id=\"activeId\"\n @select=\"openDoc\"\n @select-method=\"onSelectMethod\"\n />\n <div v-show=\"sideTab === 'libs'\" class=\"epl__libs\">\n <div class=\"epl__libsroot\">支持库</div>\n <button\n v-for=\"(l, i) in program.libraries\"\n :key=\"i\"\n type=\"button\"\n class=\"epl__lib\"\n :title=\"l.fileName\"\n @click=\"openLibraries\"\n >\n <span class=\"epl__libico\">库</span>{{ l.name || l.fileName }}\n </button>\n <p v-if=\"!program.libraries.length\" class=\"epl__libsempty\">(无引用支持库)</p>\n </div>\n </div>\n <div class=\"epl__sidetabs\">\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'libs' }\"\n @click=\"sideTab = 'libs'\"\n >\n 支持库\n </button>\n <button\n type=\"button\"\n :class=\"{ 'is-active': sideTab === 'program' }\"\n @click=\"sideTab = 'program'\"\n >\n 程序\n </button>\n </div>\n </aside>\n\n <!-- right: document + bottom tabs -->\n <main class=\"epl__main\">\n <div class=\"epl__docarea\">\n <EplDoc v-if=\"activeDoc\" :program=\"program\" :doc=\"activeDoc\" />\n <p v-else class=\"epl__msg\">从左侧选择程序集、类或表进行查看。</p>\n </div>\n <div v-if=\"openTabs.length\" class=\"epl__doctabs\">\n <div\n v-for=\"t in openTabs\"\n :key=\"t.id\"\n class=\"epl__doctab\"\n :class=\"{ 'is-active': t.id === activeId }\"\n >\n <button type=\"button\" class=\"epl__doctab-label\" @click=\"activeId = t.id\">\n {{ t.title }}\n </button>\n <button type=\"button\" class=\"epl__doctab-close\" title=\"关闭\" @click=\"closeTab(t.id)\">\n ×\n </button>\n </div>\n </div>\n </main>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.epl {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--surface);\n\n /* IDE roles follow the containing viewer, including explicit themes and host overrides. */\n --epl-bg: var(--surface);\n --epl-fg: var(--text);\n --epl-border: var(--border-2);\n --epl-th-bg: color-mix(in srgb, var(--hl-name) 12%, var(--surface));\n --epl-th-fg: var(--text-strong);\n --epl-ident: var(--hl-attr);\n --epl-keyword: var(--hl-kw);\n --epl-string: var(--hl-str);\n --epl-comment: var(--hl-name);\n --epl-check: var(--hl-name);\n --epl-guide: var(--border-2);\n --epl-gutter: var(--muted);\n --epl-gutter-bg: var(--code-bg);\n --epl-row-hover: var(--accent-soft);\n --epl-sel: color-mix(in srgb, var(--hl-attr) 18%, var(--surface));\n --epl-side-bg: var(--surface-2);\n --epl-tabbar-bg: var(--surface-2);\n --epl-tab-bg: var(--code-head);\n --epl-tab-active-bg: var(--surface);\n}\n\n/* ── status messages ─────────────────────────────────────────────────────── */\n.epl__msg {\n padding: 20px 16px;\n color: var(--muted);\n font-size: 13px;\n}\n.epl__msg--error {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n color: var(--danger);\n}\n.epl__gate {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 8px;\n}\n.epl__tip {\n color: var(--faint);\n font-size: 12.5px;\n}\n.epl__btn {\n font-family: inherit;\n font-size: 12.5px;\n color: var(--text);\n background: var(--surface);\n border: 1px solid var(--border-2);\n border-radius: 8px;\n padding: 5px 12px;\n cursor: pointer;\n}\n.epl__btn:hover {\n border-color: var(--accent);\n color: var(--accent);\n}\n\n/* ── IDE shell ───────────────────────────────────────────────────────────── */\n.epl__ide {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n background: var(--epl-bg);\n color: var(--epl-fg);\n}\n.epl__top {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding: 6px 12px;\n border-bottom: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n font-size: 12.5px;\n}\n.epl__name {\n font-weight: 600;\n color: var(--text-strong);\n}\n.epl__badge {\n font-size: 11px;\n color: var(--hl-name);\n background: color-mix(in srgb, var(--hl-name) 12%, transparent);\n border-radius: 999px;\n padding: 1px 8px;\n}\n.epl__counts {\n color: var(--muted);\n}\n\n.epl__body {\n flex: 1 1 auto;\n min-height: 0;\n display: flex;\n}\n\n/* left side */\n.epl__side {\n flex: 0 0 auto;\n width: 248px;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-side-bg);\n}\n.epl__sidebody {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n}\n.epl__sidetabs {\n flex: 0 0 auto;\n display: flex;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__sidetabs button {\n flex: 1 1 0;\n padding: 5px 8px;\n border: none;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: var(--text);\n}\n.epl__sidetabs button:last-child {\n border-right: none;\n}\n.epl__sidetabs button.is-active {\n background: var(--epl-tab-active-bg);\n font-weight: 600;\n color: var(--text-strong);\n}\n.epl__libs {\n padding: 6px 0;\n font-size: 12.5px;\n}\n.epl__libsroot {\n padding: 2px 10px 4px;\n font-weight: 600;\n color: var(--epl-th-fg);\n}\n.epl__lib {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 2px 10px 2px 18px;\n border: none;\n background: none;\n cursor: pointer;\n text-align: left;\n font: inherit;\n color: inherit;\n}\n.epl__lib:hover {\n color: var(--epl-ident);\n}\n.epl__libico {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 15px;\n height: 15px;\n border-radius: 3px;\n font-size: 10px;\n color: #fff;\n background: #c08a4a;\n}\n.epl__libsempty {\n padding: 4px 12px;\n color: var(--epl-gutter);\n}\n\n/* right side */\n.epl__main {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n}\n.epl__docarea {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n background: var(--epl-bg);\n}\n.epl__doctabs {\n flex: 0 0 auto;\n display: flex;\n overflow-x: auto;\n border-top: 1px solid var(--epl-border);\n background: var(--epl-tabbar-bg);\n}\n.epl__doctab {\n display: flex;\n align-items: center;\n border-right: 1px solid var(--epl-border);\n background: var(--epl-tab-bg);\n white-space: nowrap;\n}\n.epl__doctab.is-active {\n background: var(--epl-tab-active-bg);\n}\n.epl__doctab-label {\n padding: 5px 6px 5px 12px;\n border: none;\n background: none;\n cursor: pointer;\n font: inherit;\n font-size: 12px;\n color: var(--text);\n max-width: 16em;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.epl__doctab.is-active .epl__doctab-label {\n font-weight: 600;\n color: var(--text-strong);\n}\n.epl__doctab-close {\n padding: 0 8px 0 2px;\n border: none;\n background: none;\n cursor: pointer;\n color: var(--muted);\n font-size: 14px;\n line-height: 1;\n}\n.epl__doctab-close:hover {\n color: var(--epl-string);\n}\n</style>\n"],"mappings":""}