@brftech/filex-core 0.1.56

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/dist/ArchiveViewer-CJdaw5x3.js +99 -0
  4. package/dist/ArchiveViewer-CJdaw5x3.js.map +1 -0
  5. package/dist/CsvViewer-QYx_tePQ.js +132 -0
  6. package/dist/CsvViewer-QYx_tePQ.js.map +1 -0
  7. package/dist/DrawioViewer-GWt0TyB7.js +141 -0
  8. package/dist/DrawioViewer-GWt0TyB7.js.map +1 -0
  9. package/dist/EpubViewer-Je0ywklQ.js +146 -0
  10. package/dist/EpubViewer-Je0ywklQ.js.map +1 -0
  11. package/dist/IpynbViewer-TsB4gqP-.js +175 -0
  12. package/dist/IpynbViewer-TsB4gqP-.js.map +1 -0
  13. package/dist/MermaidViewer-CXfeDS-7.js +128 -0
  14. package/dist/MermaidViewer-CXfeDS-7.js.map +1 -0
  15. package/dist/PsdViewer-VtPpxKVe.js +113 -0
  16. package/dist/PsdViewer-VtPpxKVe.js.map +1 -0
  17. package/dist/TiffViewer-OINn9bbb.js +133 -0
  18. package/dist/TiffViewer-OINn9bbb.js.map +1 -0
  19. package/dist/UTIF-Cjsrlm0l.js +3104 -0
  20. package/dist/UTIF-Cjsrlm0l.js.map +1 -0
  21. package/dist/Viewer3D-xvvyiWHd.js +61 -0
  22. package/dist/Viewer3D-xvvyiWHd.js.map +1 -0
  23. package/dist/_commonjsHelpers-DaMA6jEr.js +9 -0
  24. package/dist/_commonjsHelpers-DaMA6jEr.js.map +1 -0
  25. package/dist/filex-core.js +26 -0
  26. package/dist/filex-core.js.map +1 -0
  27. package/dist/filex-core.umd.cjs +285 -0
  28. package/dist/filex-core.umd.cjs.map +1 -0
  29. package/dist/index-CmSYzt6L.js +5404 -0
  30. package/dist/index-CmSYzt6L.js.map +1 -0
  31. package/dist/index-UFULWo35.js +10736 -0
  32. package/dist/index-UFULWo35.js.map +1 -0
  33. package/dist/index.d.ts +1089 -0
  34. package/dist/katex-yuB6V-q6.js +11616 -0
  35. package/dist/katex-yuB6V-q6.js.map +1 -0
  36. package/dist/papaparse.min-VB1HBwYX.js +441 -0
  37. package/dist/papaparse.min-VB1HBwYX.js.map +1 -0
  38. package/dist/style.css +1 -0
  39. package/dist/useViewerFetch-czqbd2Lj.js +25 -0
  40. package/dist/useViewerFetch-czqbd2Lj.js.map +1 -0
  41. package/package.json +113 -0
  42. package/src/FileExplorer.vue +1872 -0
  43. package/src/components/Breadcrumb.vue +282 -0
  44. package/src/components/ContextMenu.vue +152 -0
  45. package/src/components/GridView.vue +166 -0
  46. package/src/components/ListView.vue +212 -0
  47. package/src/components/PendingOpsTray.vue +91 -0
  48. package/src/components/RecentlyOpened.vue +155 -0
  49. package/src/components/StarButton.vue +101 -0
  50. package/src/components/TagPicker.vue +184 -0
  51. package/src/components/Toolbar.vue +220 -0
  52. package/src/components/UploadProgress.vue +78 -0
  53. package/src/composables/useFileApi.ts +692 -0
  54. package/src/composables/useKeyboardShortcuts.ts +144 -0
  55. package/src/composables/useLocale.ts +60 -0
  56. package/src/composables/useMonacoLoader.ts +98 -0
  57. package/src/composables/usePendingOps.ts +200 -0
  58. package/src/composables/useSelection.ts +81 -0
  59. package/src/composables/useUploadChunked.ts +250 -0
  60. package/src/composables/useViewerFetch.ts +58 -0
  61. package/src/index.ts +80 -0
  62. package/src/locales/en.ts +122 -0
  63. package/src/locales/index.ts +12 -0
  64. package/src/locales/tr.ts +122 -0
  65. package/src/modals/ConvertModal.vue +254 -0
  66. package/src/modals/DeleteConfirmModal.vue +32 -0
  67. package/src/modals/Modal.vue +104 -0
  68. package/src/modals/NewFolderModal.vue +64 -0
  69. package/src/modals/PermissionsModal.vue +815 -0
  70. package/src/modals/PreviewModal.vue +1070 -0
  71. package/src/modals/RenameModal.vue +58 -0
  72. package/src/modals/ShareModal.vue +135 -0
  73. package/src/styles/base.css +1224 -0
  74. package/src/styles/variables.css +90 -0
  75. package/src/types/ExplorerConfig.ts +282 -0
  76. package/src/types/FileNode.ts +136 -0
  77. package/src/types/index.ts +27 -0
  78. package/src/types/peers.d.ts +45 -0
  79. package/src/viewers/ArchiveViewer.vue +186 -0
  80. package/src/viewers/CsvViewer.vue +319 -0
  81. package/src/viewers/DrawioViewer.vue +243 -0
  82. package/src/viewers/EpubViewer.vue +307 -0
  83. package/src/viewers/IpynbViewer.vue +365 -0
  84. package/src/viewers/MermaidViewer.vue +259 -0
  85. package/src/viewers/PdfViewer.vue +562 -0
  86. package/src/viewers/PsdViewer.vue +264 -0
  87. package/src/viewers/TiffViewer.vue +273 -0
  88. package/src/viewers/Viewer3D.vue +120 -0
@@ -0,0 +1,562 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * PdfViewer — rich PDF preview/edit via `pdfjs-dist`.
4
+ *
5
+ * Lazy-imports `pdfjs-dist/legacy/build/pdf` (~600 KB; the worker is
6
+ * loaded as a separate script). Renders pages on-demand to canvases:
7
+ * an IntersectionObserver attached to the page placeholders kicks off
8
+ * `page.render()` only for visible pages — opening a 500-page PDF
9
+ * doesn't paint 500 canvases up front.
10
+ *
11
+ * Features:
12
+ * - prev/next page jump (also typing the page number)
13
+ * - zoom in/out + fit-width
14
+ * - search box (case-insensitive, full document text scan)
15
+ * - download button
16
+ * - annotation toggle: enables form field editing via the AnnotationLayer
17
+ * - "Save annotations" — `pdf.saveDocument()` → POST to `pdfSaveUrl`
18
+ *
19
+ * Falls back to the host's native `<object>` mounting when pdfjs isn't
20
+ * installed (the parent PreviewModal handles that branch).
21
+ */
22
+ import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue';
23
+ import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
24
+
25
+ const props = defineProps<{
26
+ url: string;
27
+ filePath: string;
28
+ mime?: string;
29
+ ext: string;
30
+ pdfWorkerUrl?: string;
31
+ pdfSaveUrl?: string;
32
+ t?: (key: string) => string;
33
+ authHeaders?: () => Record<string, string>;
34
+ authCredentials?: RequestCredentials;
35
+ }>();
36
+
37
+ const emit = defineEmits<{
38
+ (e: 'fallback'): void;
39
+ }>();
40
+
41
+ const containerEl = ref<HTMLDivElement | null>(null);
42
+ const error = ref<string | null>(null);
43
+ const loading = ref(true);
44
+ const totalPages = ref(0);
45
+ const currentPage = ref(1);
46
+ const scale = ref(1);
47
+ const annotationsOn = ref(false);
48
+ const saveStatus = ref<'idle' | 'saving' | 'saved' | 'error'>('idle');
49
+ const searchQuery = ref('');
50
+ const searchResults = ref(0);
51
+
52
+ let pdfjs: any = null;
53
+ let pdfDoc: any = null;
54
+ const pageRenderState: Map<number, 'pending' | 'rendered'> = new Map();
55
+ const pageEls: Map<number, HTMLDivElement> = new Map();
56
+ let observer: IntersectionObserver | null = null;
57
+ let renderToken = 0;
58
+
59
+ async function ensurePdfjs(): Promise<any | null> {
60
+ if (pdfjs) return pdfjs;
61
+ try {
62
+ // Use the legacy build for broader browser compatibility and
63
+ // simpler worker bootstrap (no module worker requirement).
64
+ const mod = await import(/* @vite-ignore */ 'pdfjs-dist/legacy/build/pdf');
65
+ pdfjs = mod.default ?? mod;
66
+ if (pdfjs.GlobalWorkerOptions) {
67
+ const version = pdfjs.version || '4.0.379';
68
+ pdfjs.GlobalWorkerOptions.workerSrc =
69
+ props.pdfWorkerUrl ||
70
+ `https://cdn.jsdelivr.net/npm/pdfjs-dist@${version}/legacy/build/pdf.worker.min.js`;
71
+ }
72
+ return pdfjs;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ // useNativeViewer flips the renderer to <embed type="application/pdf">
79
+ // when pdfjs.getDocument throws (worker fetch blocked, CDN
80
+ // unreachable, or pdfjs not installed). All evergreen browsers ship a
81
+ // built-in PDF viewer at the embed level, so this is a graceful
82
+ // fallback rather than the "open in new tab" lifeboat the toolbar
83
+ // already exposes.
84
+ const useNativeViewer = ref(false);
85
+
86
+ async function load(): Promise<void> {
87
+ loading.value = true;
88
+ error.value = null;
89
+ searchResults.value = 0;
90
+ pageRenderState.clear();
91
+ pageEls.clear();
92
+ if (containerEl.value) containerEl.value.innerHTML = '';
93
+ if (observer) {
94
+ observer.disconnect();
95
+ observer = null;
96
+ }
97
+ if (pdfDoc) {
98
+ try { pdfDoc.destroy?.(); } catch { /* ignore */ }
99
+ pdfDoc = null;
100
+ }
101
+
102
+ const myToken = ++renderToken;
103
+
104
+ const lib = await ensurePdfjs();
105
+ if (myToken !== renderToken) return;
106
+ if (!lib) {
107
+ // pdfjs-dist absent → use the browser's built-in PDF viewer via
108
+ // <embed>. error.value stays null so the template renders the
109
+ // native preview without an angry banner.
110
+ useNativeViewer.value = true;
111
+ loading.value = false;
112
+ emit('fallback');
113
+ return;
114
+ }
115
+
116
+ try {
117
+ const buf = await fetchViewerArrayBuffer({
118
+ url: props.url,
119
+ headers: props.authHeaders?.() ?? {},
120
+ credentials: props.authCredentials,
121
+ });
122
+ if (myToken !== renderToken) return;
123
+
124
+ pdfDoc = await lib.getDocument({
125
+ data: buf,
126
+ enableXfa: true,
127
+ }).promise;
128
+
129
+ if (myToken !== renderToken) return;
130
+ totalPages.value = pdfDoc.numPages;
131
+
132
+ await nextTick();
133
+ buildPagePlaceholders();
134
+ setupObserver();
135
+ } catch (err) {
136
+ // Worker fetch blocked (CDN/CSP), unsupported runtime, etc. —
137
+ // hand off to the browser's PDF viewer instead of a dead error
138
+ // panel. Captures the original message in case operators want
139
+ // it via DevTools.
140
+ if (typeof console !== 'undefined') {
141
+ console.warn('[filex/PdfViewer] pdfjs failed, using native embed:', err);
142
+ }
143
+ useNativeViewer.value = true;
144
+ } finally {
145
+ loading.value = false;
146
+ }
147
+ }
148
+
149
+ function buildPagePlaceholders(): void {
150
+ if (!containerEl.value) return;
151
+ for (let i = 1; i <= totalPages.value; i++) {
152
+ const wrap = document.createElement('div');
153
+ wrap.className = 'filex-viewer-pdf__page';
154
+ wrap.dataset.page = String(i);
155
+ wrap.style.position = 'relative';
156
+ wrap.style.margin = '12px auto';
157
+ wrap.style.background = '#fff';
158
+ wrap.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.3)';
159
+ wrap.style.minHeight = '600px';
160
+ wrap.style.width = '816px';
161
+ containerEl.value.appendChild(wrap);
162
+ pageEls.set(i, wrap);
163
+ pageRenderState.set(i, 'pending');
164
+ }
165
+ }
166
+
167
+ function setupObserver(): void {
168
+ observer = new IntersectionObserver(
169
+ (entries) => {
170
+ for (const entry of entries) {
171
+ const el = entry.target as HTMLDivElement;
172
+ const num = Number(el.dataset.page);
173
+ if (entry.isIntersecting) {
174
+ if (pageRenderState.get(num) === 'pending') {
175
+ pageRenderState.set(num, 'rendered');
176
+ renderPage(num).catch(() => {
177
+ pageRenderState.set(num, 'pending');
178
+ });
179
+ }
180
+ if (entry.intersectionRatio > 0.5) {
181
+ currentPage.value = num;
182
+ }
183
+ }
184
+ }
185
+ },
186
+ { root: containerEl.value, threshold: [0, 0.5, 1] },
187
+ );
188
+ for (const el of pageEls.values()) {
189
+ observer.observe(el);
190
+ }
191
+ }
192
+
193
+ async function renderPage(pageNum: number): Promise<void> {
194
+ if (!pdfDoc) return;
195
+ const wrap = pageEls.get(pageNum);
196
+ if (!wrap) return;
197
+ const page = await pdfDoc.getPage(pageNum);
198
+ const viewport = page.getViewport({ scale: scale.value * 1.5 });
199
+ wrap.style.width = `${viewport.width / 1.5}px`;
200
+ wrap.style.minHeight = `${viewport.height / 1.5}px`;
201
+
202
+ const canvas = document.createElement('canvas');
203
+ canvas.width = viewport.width;
204
+ canvas.height = viewport.height;
205
+ canvas.style.width = '100%';
206
+ canvas.style.height = 'auto';
207
+ canvas.style.display = 'block';
208
+ wrap.innerHTML = '';
209
+ wrap.appendChild(canvas);
210
+
211
+ const ctx = canvas.getContext('2d');
212
+ if (!ctx) return;
213
+ await page.render({ canvasContext: ctx, viewport }).promise;
214
+
215
+ if (annotationsOn.value && pdfjs?.AnnotationLayer) {
216
+ const annoDiv = document.createElement('div');
217
+ annoDiv.className = 'filex-viewer-pdf__anno';
218
+ annoDiv.style.position = 'absolute';
219
+ annoDiv.style.inset = '0';
220
+ annoDiv.style.transformOrigin = '0 0';
221
+ wrap.appendChild(annoDiv);
222
+ try {
223
+ const annotations = await page.getAnnotations({ intent: 'display' });
224
+ pdfjs.AnnotationLayer.render({
225
+ viewport: viewport.clone({ dontFlip: true }),
226
+ div: annoDiv,
227
+ annotations,
228
+ page,
229
+ renderForms: true,
230
+ });
231
+ } catch {
232
+ /* ignore — best-effort annotation render */
233
+ }
234
+ }
235
+ }
236
+
237
+ async function rerenderAll(): Promise<void> {
238
+ for (const [num, state] of pageRenderState.entries()) {
239
+ if (state === 'rendered') {
240
+ pageRenderState.set(num, 'pending');
241
+ const el = pageEls.get(num);
242
+ if (el && observer) {
243
+ const rect = el.getBoundingClientRect();
244
+ const root = containerEl.value?.getBoundingClientRect();
245
+ if (root && rect.bottom > root.top && rect.top < root.bottom) {
246
+ pageRenderState.set(num, 'rendered');
247
+ renderPage(num).catch(() => {
248
+ pageRenderState.set(num, 'pending');
249
+ });
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ function gotoPage(n: number): void {
257
+ if (n < 1 || n > totalPages.value) return;
258
+ const el = pageEls.get(n);
259
+ el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
260
+ currentPage.value = n;
261
+ }
262
+
263
+ function next(): void {
264
+ gotoPage(currentPage.value + 1);
265
+ }
266
+ function prev(): void {
267
+ gotoPage(currentPage.value - 1);
268
+ }
269
+
270
+ function zoomIn(): void {
271
+ scale.value = Math.min(4, scale.value * 1.25);
272
+ rerenderAll();
273
+ }
274
+ function zoomOut(): void {
275
+ scale.value = Math.max(0.25, scale.value / 1.25);
276
+ rerenderAll();
277
+ }
278
+ function fitWidth(): void {
279
+ if (!containerEl.value || !pdfDoc) return;
280
+ pdfDoc
281
+ .getPage(1)
282
+ .then((p: any) => {
283
+ const v = p.getViewport({ scale: 1 });
284
+ const containerWidth = (containerEl.value?.clientWidth ?? 800) - 48;
285
+ scale.value = Math.max(0.25, Math.min(4, containerWidth / v.width));
286
+ rerenderAll();
287
+ })
288
+ .catch(() => {});
289
+ }
290
+
291
+ function toggleAnnotations(): void {
292
+ annotationsOn.value = !annotationsOn.value;
293
+ rerenderAll();
294
+ }
295
+
296
+ async function saveAnnotations(): Promise<void> {
297
+ if (!props.pdfSaveUrl || !pdfDoc) return;
298
+ saveStatus.value = 'saving';
299
+ try {
300
+ const data: Uint8Array = await pdfDoc.saveDocument();
301
+ const base64 = btoa(String.fromCharCode(...data));
302
+ const headers: Record<string, string> = {
303
+ 'Content-Type': 'application/json',
304
+ ...(props.authHeaders?.() ?? {}),
305
+ };
306
+ const res = await fetch(props.pdfSaveUrl, {
307
+ method: 'POST',
308
+ headers,
309
+ credentials: props.authCredentials || 'same-origin',
310
+ body: JSON.stringify({ path: props.filePath, base64 }),
311
+ });
312
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
313
+ saveStatus.value = 'saved';
314
+ setTimeout(() => {
315
+ if (saveStatus.value === 'saved') saveStatus.value = 'idle';
316
+ }, 2500);
317
+ } catch (err) {
318
+ saveStatus.value = 'error';
319
+ error.value = err instanceof Error ? err.message : 'save failed';
320
+ }
321
+ }
322
+
323
+ async function search(): Promise<void> {
324
+ if (!pdfDoc) return;
325
+ const q = searchQuery.value.trim().toLowerCase();
326
+ if (!q) {
327
+ searchResults.value = 0;
328
+ return;
329
+ }
330
+ let count = 0;
331
+ for (let i = 1; i <= totalPages.value; i++) {
332
+ const page = await pdfDoc.getPage(i);
333
+ const content = await page.getTextContent();
334
+ const text = content.items.map((it: any) => it.str).join(' ').toLowerCase();
335
+ let from = 0;
336
+ while ((from = text.indexOf(q, from)) !== -1) {
337
+ count++;
338
+ from += q.length;
339
+ }
340
+ }
341
+ searchResults.value = count;
342
+ }
343
+
344
+ const pageLabel = computed(() => {
345
+ const m = (props.t && props.t('viewer.page_n_of_m')) || 'Page {n} of {m}';
346
+ return m
347
+ .replace('{n}', String(currentPage.value))
348
+ .replace('{m}', String(totalPages.value));
349
+ });
350
+
351
+ onMounted(load);
352
+ onBeforeUnmount(() => {
353
+ renderToken++;
354
+ if (observer) {
355
+ observer.disconnect();
356
+ observer = null;
357
+ }
358
+ if (pdfDoc) {
359
+ try { pdfDoc.destroy?.(); } catch { /* ignore */ }
360
+ pdfDoc = null;
361
+ }
362
+ });
363
+
364
+ watch(() => props.url, load);
365
+
366
+ function tt(key: string, fallback: string): string {
367
+ return props.t ? props.t(key) : fallback;
368
+ }
369
+ </script>
370
+
371
+ <template>
372
+ <div class="filex-viewer-pdf">
373
+ <div class="filex-viewer-pdf__bar">
374
+ <button type="button" class="filex-viewer-btn" :disabled="currentPage <= 1" @click="prev">‹</button>
375
+ <input
376
+ type="number"
377
+ class="filex-viewer-pdf__page-input"
378
+ :min="1"
379
+ :max="totalPages || 1"
380
+ :value="currentPage"
381
+ @change="(ev) => gotoPage(Number((ev.target as HTMLInputElement).value))"
382
+ />
383
+ <span class="filex-viewer-pdf__total">/ {{ totalPages }}</span>
384
+ <button type="button" class="filex-viewer-btn" :disabled="currentPage >= totalPages" @click="next">›</button>
385
+ <span class="filex-viewer-spacer" />
386
+ <button type="button" class="filex-viewer-btn" @click="zoomOut" :title="tt('viewer.zoom_out', 'Zoom out')">−</button>
387
+ <span class="filex-viewer-pdf__zoom">{{ Math.round(scale * 100) }}%</span>
388
+ <button type="button" class="filex-viewer-btn" @click="zoomIn" :title="tt('viewer.zoom_in', 'Zoom in')">+</button>
389
+ <button type="button" class="filex-viewer-btn" @click="fitWidth" :title="tt('viewer.fit_width', 'Fit width')">↔</button>
390
+ <input
391
+ v-model="searchQuery"
392
+ type="search"
393
+ class="filex-viewer-pdf__search"
394
+ :placeholder="tt('viewer.search', 'Search')"
395
+ @keydown.enter="search"
396
+ />
397
+ <span v-if="searchResults > 0" class="filex-viewer-pdf__hits">{{ searchResults }}</span>
398
+ <button
399
+ type="button"
400
+ class="filex-viewer-btn"
401
+ :class="{ 'is-active': annotationsOn }"
402
+ @click="toggleAnnotations"
403
+ >
404
+ {{ annotationsOn ? tt('viewer.annotations_on', 'Annot: on') : tt('viewer.annotations_off', 'Annot: off') }}
405
+ </button>
406
+ <button
407
+ v-if="pdfSaveUrl"
408
+ type="button"
409
+ class="filex-viewer-btn"
410
+ :disabled="saveStatus === 'saving'"
411
+ @click="saveAnnotations"
412
+ >
413
+ {{
414
+ saveStatus === 'saving'
415
+ ? '⏳'
416
+ : saveStatus === 'saved'
417
+ ? '✓'
418
+ : tt('viewer.save_annotations', 'Save')
419
+ }}
420
+ </button>
421
+ </div>
422
+ <div class="filex-viewer-pdf__pane">
423
+ <!-- Browser-native PDF viewer fallback. Triggered when pdfjs
424
+ is missing or its worker can't load (CDN blocked / CSP). -->
425
+ <embed
426
+ v-if="useNativeViewer"
427
+ :src="props.url"
428
+ type="application/pdf"
429
+ class="filex-viewer-pdf__embed"
430
+ />
431
+ <div v-else-if="error" class="filex-viewer-fallback">
432
+ <span class="filex-viewer-fallback__icon">📕</span>
433
+ <p>{{ error }}</p>
434
+ </div>
435
+ <div v-else-if="loading" class="filex-viewer-fallback">
436
+ <span class="filex-viewer-fallback__icon">⏳</span>
437
+ <p>{{ tt('viewer.loading', 'Loading…') }}</p>
438
+ </div>
439
+ <div ref="containerEl" v-show="!loading && !error" class="filex-viewer-pdf__pages" />
440
+ </div>
441
+ <div v-if="!loading && !error && totalPages > 0" class="filex-viewer-pdf__footer">
442
+ {{ pageLabel }}
443
+ </div>
444
+ </div>
445
+ </template>
446
+
447
+ <style scoped>
448
+ .filex-viewer-pdf {
449
+ display: flex;
450
+ flex-direction: column;
451
+ width: 100%;
452
+ height: 100%;
453
+ min-height: 70vh;
454
+ background: var(--fe-bg, #fff);
455
+ }
456
+ .filex-viewer-pdf__embed {
457
+ width: 100%;
458
+ height: 100%;
459
+ min-height: 70vh;
460
+ border: 0;
461
+ }
462
+ .filex-viewer-pdf__bar {
463
+ display: flex;
464
+ align-items: center;
465
+ gap: 6px;
466
+ padding: 8px 12px;
467
+ background: var(--fe-bg-elev, #f7f8fa);
468
+ border-bottom: 1px solid var(--fe-border, #e2e6ed);
469
+ font-size: 13px;
470
+ flex-wrap: wrap;
471
+ }
472
+ .filex-viewer-pdf__page-input {
473
+ width: 60px;
474
+ padding: 4px 6px;
475
+ border: 1px solid var(--fe-border, #e2e6ed);
476
+ border-radius: 4px;
477
+ background: var(--fe-bg, #fff);
478
+ color: inherit;
479
+ text-align: center;
480
+ font: inherit;
481
+ font-size: 12px;
482
+ }
483
+ .filex-viewer-pdf__total {
484
+ font-size: 12px;
485
+ color: var(--fe-text-muted, #5a6475);
486
+ }
487
+ .filex-viewer-pdf__zoom {
488
+ min-width: 50px;
489
+ text-align: center;
490
+ font-variant-numeric: tabular-nums;
491
+ font-size: 12px;
492
+ }
493
+ .filex-viewer-pdf__search {
494
+ width: 160px;
495
+ padding: 4px 8px;
496
+ border: 1px solid var(--fe-border, #e2e6ed);
497
+ border-radius: 4px;
498
+ background: var(--fe-bg, #fff);
499
+ color: inherit;
500
+ font: inherit;
501
+ font-size: 12px;
502
+ }
503
+ .filex-viewer-pdf__hits {
504
+ font-size: 11px;
505
+ background: var(--fe-bg-selected, #dfe8ff);
506
+ color: var(--fe-primary, #3b82f6);
507
+ padding: 2px 6px;
508
+ border-radius: 8px;
509
+ }
510
+ .filex-viewer-spacer { flex: 1; }
511
+ .filex-viewer-pdf__pane {
512
+ flex: 1;
513
+ overflow: auto;
514
+ padding: 16px;
515
+ background: #2a2d33;
516
+ }
517
+ .filex-viewer-pdf__pages {
518
+ display: flex;
519
+ flex-direction: column;
520
+ align-items: center;
521
+ }
522
+ .filex-viewer-pdf__footer {
523
+ padding: 6px 12px;
524
+ background: var(--fe-bg-elev, #f7f8fa);
525
+ border-top: 1px solid var(--fe-border, #e2e6ed);
526
+ font-size: 11px;
527
+ color: var(--fe-text-muted, #5a6475);
528
+ text-align: center;
529
+ }
530
+ .filex-viewer-fallback {
531
+ text-align: center;
532
+ padding: 32px;
533
+ color: #c8cdd6;
534
+ margin: auto;
535
+ }
536
+ .filex-viewer-fallback__icon {
537
+ font-size: 48px;
538
+ display: block;
539
+ margin-bottom: 12px;
540
+ }
541
+ .filex-viewer-btn {
542
+ border: 1px solid var(--fe-border, #e2e6ed);
543
+ background: var(--fe-bg, #fff);
544
+ color: var(--fe-text, #1a1e27);
545
+ padding: 4px 10px;
546
+ border-radius: 4px;
547
+ cursor: pointer;
548
+ font: inherit;
549
+ font-size: 12px;
550
+ }
551
+ .filex-viewer-btn:hover:not(:disabled) {
552
+ background: var(--fe-bg-hover, #edf0f5);
553
+ }
554
+ .filex-viewer-btn:disabled {
555
+ opacity: 0.4;
556
+ cursor: not-allowed;
557
+ }
558
+ .filex-viewer-btn.is-active {
559
+ background: var(--fe-bg-selected, #dfe8ff);
560
+ border-color: var(--fe-primary, #3b82f6);
561
+ }
562
+ </style>