@brftech/filex-core 0.40.0 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +32 -25
  2. package/dist/ArchiveViewer-DxxjhYZD.js +124 -0
  3. package/dist/ArchiveViewer-DxxjhYZD.js.map +1 -0
  4. package/dist/CsvViewer-CyuwENI_.js +141 -0
  5. package/dist/CsvViewer-CyuwENI_.js.map +1 -0
  6. package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-BIYsTX97.js} +48 -42
  7. package/dist/DrawioViewer-BIYsTX97.js.map +1 -0
  8. package/dist/EpubViewer-K9PjCMmS.js +163 -0
  9. package/dist/EpubViewer-K9PjCMmS.js.map +1 -0
  10. package/dist/IpynbViewer-DYU6Zgz2.js +184 -0
  11. package/dist/IpynbViewer-DYU6Zgz2.js.map +1 -0
  12. package/dist/MermaidViewer-Bp73aId8.js +137 -0
  13. package/dist/MermaidViewer-Bp73aId8.js.map +1 -0
  14. package/dist/PsdViewer-C2eLivw1.js +122 -0
  15. package/dist/PsdViewer-C2eLivw1.js.map +1 -0
  16. package/dist/TiffViewer-DsHg5gcz.js +142 -0
  17. package/dist/TiffViewer-DsHg5gcz.js.map +1 -0
  18. package/dist/Viewer3D-C_dc2HN9.js +75 -0
  19. package/dist/Viewer3D-C_dc2HN9.js.map +1 -0
  20. package/dist/filex-core.js +218 -20361
  21. package/dist/filex-core.js.map +1 -1
  22. package/dist/filex-core.umd.cjs +61 -62
  23. package/dist/filex-core.umd.cjs.map +1 -1
  24. package/dist/index-vxMZpYb-.js +26700 -0
  25. package/dist/index-vxMZpYb-.js.map +1 -0
  26. package/dist/index.d.ts +4212 -862
  27. package/dist/style.css +1 -1
  28. package/package.json +1 -1
  29. package/src/FileExplorer.vue +2154 -506
  30. package/src/components/AdvancedSearch.vue +870 -0
  31. package/src/components/Breadcrumb.vue +191 -19
  32. package/src/components/CommandPalette.vue +66 -22
  33. package/src/components/ConnectionsPanel.vue +6 -2
  34. package/src/components/ContextMenu.vue +39 -2
  35. package/src/components/EncryptedFolderModal.vue +31 -25
  36. package/src/components/FilePane.vue +951 -0
  37. package/src/components/FilterBar.vue +568 -72
  38. package/src/components/GalleryView.vue +172 -56
  39. package/src/components/GridView.vue +331 -82
  40. package/src/components/HomeView.vue +331 -0
  41. package/src/components/InspectorPanel.vue +296 -53
  42. package/src/components/ListView.vue +1173 -194
  43. package/src/components/NFSExportsPanel.vue +7 -4
  44. package/src/components/OnboardingTour.vue +156 -33
  45. package/src/components/QuickLook.vue +26 -1
  46. package/src/components/RecentlyOpened.vue +17 -2
  47. package/src/components/S3KeysPanel.vue +7 -4
  48. package/src/components/SSHKeysPanel.vue +7 -4
  49. package/src/components/ShortcutSettings.vue +6 -2
  50. package/src/components/ShortcutsHelp.vue +4 -1
  51. package/src/components/SideNav.vue +206 -140
  52. package/src/components/StorageFields.vue +19 -1
  53. package/src/components/TabBar.vue +87 -1
  54. package/src/components/TagPicker.vue +77 -6
  55. package/src/components/ThemeGallery.vue +13 -198
  56. package/src/components/ThemePalette.vue +244 -0
  57. package/src/components/TimeZoneDialog.vue +135 -0
  58. package/src/components/TimeZonePicker.vue +696 -0
  59. package/src/components/TokensPanel.vue +35 -14
  60. package/src/components/Toolbar.vue +953 -465
  61. package/src/components/ViewSwitcher.vue +9 -4
  62. package/src/composables/useConnections.ts +35 -4
  63. package/src/composables/useExplorerTimeZone.ts +89 -0
  64. package/src/composables/useFileApi.ts +53 -4
  65. package/src/composables/useLocale.ts +382 -26
  66. package/src/composables/useRowTouch.ts +95 -0
  67. package/src/index.ts +177 -3
  68. package/src/lib/actionIcons.ts +330 -0
  69. package/src/lib/advSearch.ts +162 -0
  70. package/src/lib/dateGroups.ts +197 -0
  71. package/src/lib/destinationTree.ts +235 -0
  72. package/src/lib/downloadSelection.ts +154 -0
  73. package/src/lib/fileFilters.ts +319 -12
  74. package/src/lib/fileIcons.ts +299 -5
  75. package/src/lib/filePreview.ts +605 -0
  76. package/src/lib/listing.ts +72 -0
  77. package/src/lib/shareTtl.ts +31 -2
  78. package/src/lib/sortOrder.ts +459 -0
  79. package/src/lib/themes.ts +151 -116
  80. package/src/lib/timezone.ts +501 -0
  81. package/src/lib/uiProfile.ts +82 -0
  82. package/src/lib/viewPrefs.ts +1036 -0
  83. package/src/locales/en.ts +416 -26
  84. package/src/locales/tr.ts +409 -26
  85. package/src/modals/ConvertModal.vue +23 -3
  86. package/src/modals/DestinationPickerModal.vue +341 -0
  87. package/src/modals/Modal.vue +18 -4
  88. package/src/modals/NewDocumentModal.vue +525 -0
  89. package/src/modals/NewFolderModal.vue +15 -9
  90. package/src/modals/PermissionsModal.vue +557 -476
  91. package/src/modals/PreviewModal.vue +516 -156
  92. package/src/styles/base.css +5409 -8
  93. package/src/styles/variables.css +139 -65
  94. package/src/types/Connections.ts +9 -0
  95. package/src/types/ExplorerConfig.ts +144 -39
  96. package/src/types/FileNode.ts +28 -0
  97. package/src/viewers/ArchiveViewer.vue +46 -8
  98. package/src/viewers/CsvViewer.vue +19 -2
  99. package/src/viewers/DrawioViewer.vue +12 -1
  100. package/src/viewers/EpubViewer.vue +50 -6
  101. package/src/viewers/IpynbViewer.vue +19 -2
  102. package/src/viewers/MermaidViewer.vue +20 -3
  103. package/src/viewers/PdfViewer.vue +24 -4
  104. package/src/viewers/PsdViewer.vue +28 -4
  105. package/src/viewers/TiffViewer.vue +19 -2
  106. package/src/viewers/Viewer3D.vue +36 -2
  107. package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
  108. package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
  109. package/dist/CsvViewer-CqWeV8VO.js +0 -131
  110. package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
  111. package/dist/DrawioViewer-BNALOB04.js.map +0 -1
  112. package/dist/EpubViewer-BPmlImG7.js +0 -145
  113. package/dist/EpubViewer-BPmlImG7.js.map +0 -1
  114. package/dist/IpynbViewer-CDlQmuCV.js +0 -175
  115. package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
  116. package/dist/MermaidViewer-sfZWdR-d.js +0 -127
  117. package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
  118. package/dist/PsdViewer-DwuySZsb.js +0 -112
  119. package/dist/PsdViewer-DwuySZsb.js.map +0 -1
  120. package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
  121. package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
  122. package/dist/Viewer3D-CJJJgkuv.js +0 -60
  123. package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
  124. package/src/components/SecondaryPane.vue +0 -447
  125. package/src/modals/ShareModal.vue +0 -139
@@ -10,6 +10,8 @@
10
10
  * decide whether to extract or download.
11
11
  */
12
12
  import { computed, onMounted, ref, watch } from 'vue';
13
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
14
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
13
15
 
14
16
  interface ArchiveEntry {
15
17
  name: string;
@@ -22,7 +24,7 @@ const props = defineProps<{
22
24
  url: string;
23
25
  filePath?: string;
24
26
  ext: string;
25
- t?: (key: string) => string;
27
+ t?: (key: string, vars?: Record<string, string | number>) => string;
26
28
  authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
27
29
  authCredentials?: RequestCredentials;
28
30
  /** ⚠ Where to ask. This used to be the literal '/api/files/archive/list',
@@ -37,8 +39,11 @@ const entries = ref<ArchiveEntry[]>([]);
37
39
  const loading = ref(true);
38
40
  const error = ref<string | null>(null);
39
41
 
40
- function tt(key: string, fallback: string): string {
41
- return props.t ? props.t(key) : fallback;
42
+ /** ⚠ `vars` go THROUGH `t()`, not into a `.replace()` afterwards: `t()` is what
43
+ * picks the singular (`viewer.archive.entries_one`) from the count, and it can
44
+ * only do that if it is told the count. Same shape CsvViewer's helper has. */
45
+ function tt(key: string, fallback: string, vars?: Record<string, string | number>): string {
46
+ return props.t ? props.t(key, vars) : fallback;
42
47
  }
43
48
 
44
49
  function fmtSize(n: number): string {
@@ -87,25 +92,54 @@ watch(() => props.filePath, load);
87
92
 
88
93
  const totalSize = computed(() => entries.value.reduce((sum, e) => sum + (e.size || 0), 0));
89
94
  const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
95
+
96
+ /* === ikon:emoji — the fallback screen's mark ==========================
97
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
98
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
99
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
100
+ * the person just clicked is wearing, so the fallback is recognisably about
101
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
102
+ * still picture can say "still going". */
103
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
104
+
105
+ /** A row inside the archive, drawn the way the listing draws the same kind. */
106
+ function entryTile(e: ArchiveEntry): string {
107
+ const name = e.name || '';
108
+ const dot = name.lastIndexOf('.');
109
+ return fileIconTile({
110
+ type: e.is_dir ? 'dir' : 'file',
111
+ extension: dot > 0 ? name.slice(dot + 1) : '',
112
+ basename: name,
113
+ });
114
+ }
90
115
  </script>
91
116
 
92
117
  <template>
93
118
  <div class="filex-viewer-archive">
94
119
  <div v-if="error" class="filex-viewer-fallback">
95
- <span class="filex-viewer-fallback__icon">🗜️</span>
120
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
121
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
96
122
  <p>{{ error }}</p>
97
123
  </div>
98
124
  <div v-else-if="loading" class="filex-viewer-fallback">
99
- <span class="filex-viewer-fallback__icon">⏳</span>
125
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
126
+ <span
127
+ class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
128
+ aria-hidden="true"
129
+ v-html="actionIconSvg('progress')"
130
+ ></span>
100
131
  <p>{{ tt('viewer.loading', 'Loading…') }}</p>
101
132
  </div>
102
133
  <div v-else-if="entries.length === 0" class="filex-viewer-fallback">
103
- <span class="filex-viewer-fallback__icon">🗜️</span>
134
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
135
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
104
136
  <p>{{ tt('viewer.archive.empty', 'Archive is empty.') }}</p>
105
137
  </div>
106
138
  <div v-else class="filex-viewer-archive__pane">
107
139
  <div class="filex-viewer-archive__summary">
108
- {{ fileCount }} {{ tt('viewer.archive.entries', '{n} files').replace('{n}', String(fileCount)) }}
140
+ <!-- The count is IN the message ("{n} files"). A `{{ fileCount }}`
141
+ printed in front of it read "3 3 files". -->
142
+ {{ tt('viewer.archive.entries', `${fileCount} files`, { n: fileCount }) }}
109
143
  · {{ fmtSize(totalSize) }}
110
144
  </div>
111
145
  <table class="filex-viewer-archive__table">
@@ -118,7 +152,11 @@ const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
118
152
  <tbody>
119
153
  <tr v-for="(e, i) in entries" :key="i" :data-dir="e.is_dir ? '1' : '0'">
120
154
  <td>
121
- <span class="filex-viewer-archive__icon">{{ e.is_dir ? '📁' : '📄' }}</span>
155
+ <!-- ikon:emoji the same tile the listing draws for the same
156
+ kind of file, so an archive's contents and the folder it
157
+ came from read as one product. -->
158
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
159
+ <span class="filex-viewer-archive__icon" aria-hidden="true" v-html="entryTile(e)"></span>
122
160
  {{ e.name }}
123
161
  </td>
124
162
  <td class="filex-viewer-archive__size">{{ e.is_dir ? '' : fmtSize(e.size) }}</td>
@@ -14,6 +14,8 @@
14
14
  * - tab vs comma auto-detection (manual override via toolbar)
15
15
  */
16
16
  import { computed, onMounted, ref, watch } from 'vue';
17
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
18
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
17
19
  import { fetchViewerText } from '../composables/useViewerFetch';
18
20
 
19
21
  const props = defineProps<{
@@ -147,17 +149,32 @@ watch(filter, () => {
147
149
  function tt(key: string, fallback: string, vars?: Record<string, string | number>): string {
148
150
  return props.t ? props.t(key, vars) : fallback;
149
151
  }
152
+
153
+ /* === ikon:emoji — the fallback screen's mark ==========================
154
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
155
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
156
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
157
+ * the person just clicked is wearing, so the fallback is recognisably about
158
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
159
+ * still picture can say "still going". */
160
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
150
161
  </script>
151
162
 
152
163
  <template>
153
164
  <div class="filex-viewer-csv">
154
165
  <div class="filex-viewer-csv__pane">
155
166
  <div v-if="error" class="filex-viewer-fallback">
156
- <span class="filex-viewer-fallback__icon">📊</span>
167
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
168
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
157
169
  <p>{{ error }}</p>
158
170
  </div>
159
171
  <div v-else-if="loading" class="filex-viewer-fallback">
160
- <span class="filex-viewer-fallback__icon">⏳</span>
172
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
173
+ <span
174
+ class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
175
+ aria-hidden="true"
176
+ v-html="actionIconSvg('progress')"
177
+ ></span>
161
178
  <p>{{ tt('viewer.loading', 'Loading…') }}</p>
162
179
  </div>
163
180
  <table v-else class="filex-viewer-csv__table">
@@ -15,6 +15,7 @@
15
15
  * (drawio uses the same shape: POST `{path, content}` body).
16
16
  */
17
17
  import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
18
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
18
19
  import { fetchViewerText } from '../composables/useViewerFetch';
19
20
 
20
21
  const props = defineProps<{
@@ -180,12 +181,22 @@ onBeforeUnmount(() => {
180
181
  function tt(key: string, fallback: string): string {
181
182
  return props.t ? props.t(key) : fallback;
182
183
  }
184
+
185
+ /* === ikon:emoji — the fallback screen's mark ==========================
186
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
187
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
188
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
189
+ * the person just clicked is wearing, so the fallback is recognisably about
190
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
191
+ * still picture can say "still going". */
192
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
183
193
  </script>
184
194
 
185
195
  <template>
186
196
  <div class="filex-viewer-drawio">
187
197
  <div v-if="error" class="filex-viewer-fallback">
188
- <span class="filex-viewer-fallback__icon">📐</span>
198
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
199
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
189
200
  <p>{{ error }}</p>
190
201
  </div>
191
202
  <iframe
@@ -13,7 +13,8 @@
13
13
  * - font size +/- (Themes API)
14
14
  * - graceful fallback if peer not installed
15
15
  */
16
- import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
16
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
17
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
17
18
 
18
19
  const props = defineProps<{
19
20
  url: string;
@@ -42,6 +43,29 @@ const loading = ref(true);
42
43
  let book: any = null;
43
44
  let rendition: any = null;
44
45
 
46
+ /**
47
+ * How long an epub.js step may hang before we call it a failure.
48
+ *
49
+ * Generous on purpose: a large publication on a slow disk legitimately takes
50
+ * seconds, and a false "cannot open" on a book that would have rendered is a
51
+ * worse bug than the one this guards against.
52
+ */
53
+ const EPUB_STEP_TIMEOUT_MS = 15000;
54
+
55
+ /** Reject if `p` has not settled within `EPUB_STEP_TIMEOUT_MS`. */
56
+ function withTimeout<T>(p: Promise<T>, label: string): Promise<T> {
57
+ let timer: ReturnType<typeof setTimeout>;
58
+ return Promise.race([
59
+ p,
60
+ new Promise<never>((_, reject) => {
61
+ timer = setTimeout(
62
+ () => reject(new Error(`EPUB ${label} timed out after ${EPUB_STEP_TIMEOUT_MS}ms`)),
63
+ EPUB_STEP_TIMEOUT_MS,
64
+ );
65
+ }),
66
+ ]).finally(() => clearTimeout(timer)) as Promise<T>;
67
+ }
68
+
45
69
  async function load(): Promise<void> {
46
70
  loading.value = true;
47
71
  ready.value = false;
@@ -81,14 +105,24 @@ async function load(): Promise<void> {
81
105
  flow: 'paginated',
82
106
  manager: 'default',
83
107
  });
84
- await rendition.display();
108
+ // ⚠⚠ Both of these have to be raced against a clock. epub.js resolves
109
+ // `display()` and `loaded.navigation` from its own internal promises, and
110
+ // when the archive is not a readable EPUB it simply NEVER settles —
111
+ // neither resolve nor reject — so the `catch` below can't fire and
112
+ // `finally` never runs. Measured 2026-09-13 on an 8-byte `handbook.epub`:
113
+ // the pane sat on "Yükleniyor…" forever with no error anywhere.
114
+ await withTimeout(rendition.display(), 'display');
85
115
  rendition.themes.fontSize(fontSize.value + '%');
86
- const nav = await book.loaded.navigation;
116
+ // `<any>` explicitly: `book` is `any`, so without it `T` infers as `{}`
117
+ // and `nav.toc` stops type-checking.
118
+ const nav = await withTimeout<any>(book.loaded.navigation, 'navigation');
87
119
  toc.value = (nav?.toc ?? []) as TocNode[];
88
120
  ready.value = true;
89
121
  } catch (err) {
90
- error.value =
91
- err instanceof Error ? err.message : 'EPUB load failed';
122
+ // The reader gets the localized sentence; epub.js's own wording (and the
123
+ // timeout marker) goes to the console for whoever is debugging.
124
+ console.warn('[filex] EPUB load failed:', err);
125
+ error.value = tt('viewer.failed_to_load', 'Failed to load file');
92
126
  } finally {
93
127
  loading.value = false;
94
128
  }
@@ -165,12 +199,22 @@ watch(
165
199
  function tt(key: string, fallback: string): string {
166
200
  return props.t ? props.t(key) : fallback;
167
201
  }
202
+
203
+ /* === ikon:emoji — the fallback screen's mark ==========================
204
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
205
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
206
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
207
+ * the person just clicked is wearing, so the fallback is recognisably about
208
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
209
+ * still picture can say "still going". */
210
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
168
211
  </script>
169
212
 
170
213
  <template>
171
214
  <div ref="root" class="filex-viewer-epub">
172
215
  <div v-if="error" class="filex-viewer-fallback">
173
- <span class="filex-viewer-fallback__icon">📖</span>
216
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
217
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
174
218
  <p>{{ error }}</p>
175
219
  </div>
176
220
  <template v-else>
@@ -14,6 +14,8 @@
14
14
  * notebook doesn't surprise the user.
15
15
  */
16
16
  import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
17
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
18
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
17
19
  import { fetchViewerText } from '../composables/useViewerFetch';
18
20
  import { ensureHighlight } from '../composables/useMonacoLoader';
19
21
 
@@ -192,17 +194,32 @@ const stats = computed(() => {
192
194
  function tt(key: string, fallback: string): string {
193
195
  return props.t ? props.t(key) : fallback;
194
196
  }
197
+
198
+ /* === ikon:emoji — the fallback screen's mark ==========================
199
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
200
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
201
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
202
+ * the person just clicked is wearing, so the fallback is recognisably about
203
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
204
+ * still picture can say "still going". */
205
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
195
206
  </script>
196
207
 
197
208
  <template>
198
209
  <div class="filex-viewer-ipynb">
199
210
  <div class="filex-viewer-ipynb__pane">
200
211
  <div v-if="error" class="filex-viewer-fallback">
201
- <span class="filex-viewer-fallback__icon">📓</span>
212
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
213
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
202
214
  <p>{{ error }}</p>
203
215
  </div>
204
216
  <div v-else-if="loading" class="filex-viewer-fallback">
205
- <span class="filex-viewer-fallback__icon">⏳</span>
217
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
218
+ <span
219
+ class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
220
+ aria-hidden="true"
221
+ v-html="actionIconSvg('progress')"
222
+ ></span>
206
223
  <p>{{ tt('viewer.loading', 'Loading…') }}</p>
207
224
  </div>
208
225
  <div v-else>
@@ -8,7 +8,9 @@
8
8
  * SVG node is inserted into a wrapper that supports pan + scale via
9
9
  * pure CSS transforms (no extra panzoom dep).
10
10
  */
11
- import { onBeforeUnmount, onMounted, ref } from 'vue';
11
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
12
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
13
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
12
14
  import { fetchViewerText } from '../composables/useViewerFetch';
13
15
 
14
16
  const props = defineProps<{
@@ -142,6 +144,15 @@ onBeforeUnmount(() => {
142
144
  function tt(key: string, fallback: string): string {
143
145
  return props.t ? props.t(key) : fallback;
144
146
  }
147
+
148
+ /* === ikon:emoji — the fallback screen's mark ==========================
149
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
150
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
151
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
152
+ * the person just clicked is wearing, so the fallback is recognisably about
153
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
154
+ * still picture can say "still going". */
155
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
145
156
  </script>
146
157
 
147
158
  <template>
@@ -158,14 +169,20 @@ function tt(key: string, fallback: string): string {
158
169
  v-if="error"
159
170
  class="filex-viewer-fallback"
160
171
  >
161
- <span class="filex-viewer-fallback__icon">📊</span>
172
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
173
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
162
174
  <p>{{ error }}</p>
163
175
  </div>
164
176
  <div
165
177
  v-else-if="loading"
166
178
  class="filex-viewer-fallback"
167
179
  >
168
- <span class="filex-viewer-fallback__icon">⏳</span>
180
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
181
+ <span
182
+ class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
183
+ aria-hidden="true"
184
+ v-html="actionIconSvg('progress')"
185
+ ></span>
169
186
  <p>{{ tt('viewer.loading', 'Loading…') }}</p>
170
187
  </div>
171
188
  <div
@@ -20,6 +20,8 @@
20
20
  * installed (the parent PreviewModal handles that branch).
21
21
  */
22
22
  import { computed, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue';
23
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
24
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
23
25
  import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
24
26
 
25
27
  const props = defineProps<{
@@ -366,6 +368,15 @@ watch(() => props.url, load);
366
368
  function tt(key: string, fallback: string): string {
367
369
  return props.t ? props.t(key) : fallback;
368
370
  }
371
+
372
+ /* === ikon:emoji — the fallback screen's mark ==========================
373
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
374
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
375
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
376
+ * the person just clicked is wearing, so the fallback is recognisably about
377
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
378
+ * still picture can say "still going". */
379
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
369
380
  </script>
370
381
 
371
382
  <template>
@@ -410,11 +421,14 @@ function tt(key: string, fallback: string): string {
410
421
  :disabled="saveStatus === 'saving'"
411
422
  @click="saveAnnotations"
412
423
  >
424
+ <!-- ikon:emoji — the button said ⏳ / ✓, an emoji and a dingbat, in
425
+ a row of word buttons. Words are what this control has always
426
+ been; the two transient states get theirs. -->
413
427
  {{
414
428
  saveStatus === 'saving'
415
- ? ''
429
+ ? tt('viewer.saving', 'Saving…')
416
430
  : saveStatus === 'saved'
417
- ? ''
431
+ ? tt('viewer.saved', 'Saved')
418
432
  : tt('viewer.save_annotations', 'Save')
419
433
  }}
420
434
  </button>
@@ -429,11 +443,17 @@ function tt(key: string, fallback: string): string {
429
443
  class="filex-viewer-pdf__embed"
430
444
  />
431
445
  <div v-else-if="error" class="filex-viewer-fallback">
432
- <span class="filex-viewer-fallback__icon">📕</span>
446
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
447
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
433
448
  <p>{{ error }}</p>
434
449
  </div>
435
450
  <div v-else-if="loading" class="filex-viewer-fallback">
436
- <span class="filex-viewer-fallback__icon">⏳</span>
451
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
452
+ <span
453
+ class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
454
+ aria-hidden="true"
455
+ v-html="actionIconSvg('progress')"
456
+ ></span>
437
457
  <p>{{ tt('viewer.loading', 'Loading…') }}</p>
438
458
  </div>
439
459
  <div ref="containerEl" v-show="!loading && !error" class="filex-viewer-pdf__pages" />
@@ -9,7 +9,9 @@
9
9
  * — left as TODO for V2 since `ag-psd` doesn't ship a composite
10
10
  * pipeline of its own).
11
11
  */
12
- import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
12
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
13
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
14
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
13
15
  import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
14
16
 
15
17
  const props = defineProps<{
@@ -108,7 +110,14 @@ async function load(): Promise<void> {
108
110
  flattenLayers(psd, flat);
109
111
  layers.value = flat;
110
112
  } catch (err) {
111
- error.value = err instanceof Error ? err.message : 'PSD decode failed';
113
+ // The person gets the localized sentence; the decoder's own words go to
114
+ // the console for whoever is debugging. `ag-psd` throws raw internals
115
+ // ("Offset is outside the bounds of the DataView" on a truncated file),
116
+ // and printing that verbatim put an untranslated English stack message in
117
+ // the middle of a Turkish UI — measured 2026-09-13 on a 6-byte
118
+ // `mockup.psd`.
119
+ console.warn('[filex] PSD decode failed:', err);
120
+ error.value = tt('viewer.failed_to_load', 'Failed to load file');
112
121
  } finally {
113
122
  loading.value = false;
114
123
  }
@@ -133,17 +142,32 @@ watch(() => props.url, load);
133
142
  function tt(key: string, fallback: string): string {
134
143
  return props.t ? props.t(key) : fallback;
135
144
  }
145
+
146
+ /* === ikon:emoji — the fallback screen's mark ==========================
147
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
148
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
149
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
150
+ * the person just clicked is wearing, so the fallback is recognisably about
151
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
152
+ * still picture can say "still going". */
153
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
136
154
  </script>
137
155
 
138
156
  <template>
139
157
  <div class="filex-viewer-psd">
140
158
  <div class="filex-viewer-psd__pane">
141
159
  <div v-if="error" class="filex-viewer-fallback">
142
- <span class="filex-viewer-fallback__icon">🎨</span>
160
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
161
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
143
162
  <p>{{ error }}</p>
144
163
  </div>
145
164
  <div v-else-if="loading" class="filex-viewer-fallback">
146
- <span class="filex-viewer-fallback__icon">⏳</span>
165
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
166
+ <span
167
+ class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
168
+ aria-hidden="true"
169
+ v-html="actionIconSvg('progress')"
170
+ ></span>
147
171
  <p>{{ tt('viewer.loading', 'Loading…') }}</p>
148
172
  </div>
149
173
  <canvas
@@ -9,6 +9,8 @@
9
9
  * conversion.
10
10
  */
11
11
  import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
12
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
13
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
12
14
  import { fetchViewerArrayBuffer } from '../composables/useViewerFetch';
13
15
 
14
16
  const props = defineProps<{
@@ -143,17 +145,32 @@ const pageLabel = computed(() => {
143
145
  function tt(key: string, fallback: string): string {
144
146
  return props.t ? props.t(key) : fallback;
145
147
  }
148
+
149
+ /* === ikon:emoji — the fallback screen's mark ==========================
150
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
151
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
152
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
153
+ * the person just clicked is wearing, so the fallback is recognisably about
154
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
155
+ * still picture can say "still going". */
156
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
146
157
  </script>
147
158
 
148
159
  <template>
149
160
  <div class="filex-viewer-tiff">
150
161
  <div class="filex-viewer-tiff__pane">
151
162
  <div v-if="error" class="filex-viewer-fallback">
152
- <span class="filex-viewer-fallback__icon">🖼️</span>
163
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
164
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
153
165
  <p>{{ error }}</p>
154
166
  </div>
155
167
  <div v-else-if="loading" class="filex-viewer-fallback">
156
- <span class="filex-viewer-fallback__icon">⏳</span>
168
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
169
+ <span
170
+ class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
171
+ aria-hidden="true"
172
+ v-html="actionIconSvg('progress')"
173
+ ></span>
157
174
  <p>{{ tt('viewer.loading', 'Loading…') }}</p>
158
175
  </div>
159
176
  <canvas
@@ -19,6 +19,8 @@
19
19
  * (sweep-2026-05-09 bug 21.)
20
20
  */
21
21
  import { computed, onMounted, ref, watch } from 'vue';
22
+ import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
23
+ import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
22
24
 
23
25
  const props = defineProps<{
24
26
  url: string;
@@ -57,6 +59,22 @@ async function load(): Promise<void> {
57
59
  }
58
60
  }
59
61
 
62
+ /**
63
+ * model-viewer failed to FETCH or PARSE the model.
64
+ *
65
+ * ⚠ `ready` only means the ~80 KB web component imported. The model itself is
66
+ * fetched and decoded by model-viewer AFTER it mounts, and when that throws
67
+ * (a truncated or non-glTF file) it renders an empty canvas and reports the
68
+ * failure only on this event. Without the listener the person got a silent
69
+ * black rectangle and nothing else — measured 2026-09-13 on an 8-byte
70
+ * `turbine.glb`, where the console had `RangeError: Invalid DataView length 12`
71
+ * and the pane had no text at all.
72
+ */
73
+ function onModelError(): void {
74
+ ready.value = false;
75
+ error.value = props.t ? props.t('viewer.failed_to_load') : 'Failed to load file';
76
+ }
77
+
60
78
  onMounted(load);
61
79
 
62
80
  watch(() => props.url, () => {
@@ -64,6 +82,15 @@ watch(() => props.url, () => {
64
82
  // fallback message).
65
83
  load();
66
84
  });
85
+
86
+ /* === ikon:emoji — the fallback screen's mark ==========================
87
+ * Every viewer opened its "cannot show this" / "still loading" screen with a
88
+ * 48px colour emoji, one per format, each from whatever emoji font the OS
89
+ * shipped. The format mark is `lib/fileIcons`'s tile — the SAME tile the row
90
+ * the person just clicked is wearing, so the fallback is recognisably about
91
+ * that file — and "loading" is the stroked ring, spun by CSS, because no
92
+ * still picture can say "still going". */
93
+ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ext }));
67
94
  </script>
68
95
 
69
96
  <template>
@@ -77,13 +104,20 @@ watch(() => props.url, () => {
77
104
  shadow-intensity="1"
78
105
  :alt="ext + ' model'"
79
106
  style="width: 100%; height: 100%; min-height: 480px; display: block"
107
+ @error="onModelError"
80
108
  />
81
109
  <div v-else-if="error" class="filex-viewer-fallback">
82
- <span class="filex-viewer-fallback__icon">📦</span>
110
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
111
+ <span class="filex-viewer-fallback__icon" aria-hidden="true" v-html="typeTile"></span>
83
112
  <p>{{ error }}</p>
84
113
  </div>
85
114
  <div v-else class="filex-viewer-fallback">
86
- <span class="filex-viewer-fallback__icon">⏳</span>
115
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons + lib/actionIcons -->
116
+ <span
117
+ class="filex-viewer-fallback__icon filex-viewer-fallback__icon--spin"
118
+ aria-hidden="true"
119
+ v-html="actionIconSvg('progress')"
120
+ ></span>
87
121
  <p>{{ t ? t('viewer.loading') : 'Loading…' }}</p>
88
122
  </div>
89
123
  </div>