@brftech/filex-core 0.42.1 → 0.43.0

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 (233) hide show
  1. package/README.md +14 -2
  2. package/dist/ArchiveViewer-BqU761Wk.js +149 -0
  3. package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
  4. package/dist/CsvViewer-ByHVK5ga.js +172 -0
  5. package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
  6. package/dist/{DrawioViewer-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-DS3mb3k8.js → EpubViewer-DJwUKlHW.js} +50 -50
  9. package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
  10. package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
  11. package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
  12. package/dist/MermaidViewer-CEDTD_pp.js +136 -0
  13. package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
  14. package/dist/PsdViewer-CZcxUpZO.js +121 -0
  15. package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
  16. package/dist/TiffViewer-BwI62CbQ.js +141 -0
  17. package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
  18. package/dist/{Viewer3D-CIrOn63I.js → Viewer3D-DoeZcuMm.js} +16 -16
  19. package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
  20. package/dist/filex-core.js +475 -219
  21. package/dist/filex-core.umd.cjs +165 -108
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/fonts/caveat-latin-ext.woff2 +0 -0
  24. package/dist/fonts/caveat-latin.woff2 +0 -0
  25. package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
  26. package/dist/fonts/dancing-script-latin.woff2 +0 -0
  27. package/dist/fonts/homemade-apple-latin.woff2 +0 -0
  28. package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
  29. package/dist/fonts/source-serif-4-latin.woff2 +0 -0
  30. package/dist/index-DuWnGuv5.js +35901 -0
  31. package/dist/index-DuWnGuv5.js.map +1 -0
  32. package/dist/index.d.ts +5241 -553
  33. package/dist/pdf-CaPYsOUo.js +21302 -0
  34. package/dist/pdf-CaPYsOUo.js.map +1 -0
  35. package/dist/style.css +1 -1
  36. package/package.json +1 -1
  37. package/src/FileExplorer.vue +1207 -343
  38. package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
  39. package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
  40. package/src/assets/fonts/README.md +58 -0
  41. package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
  42. package/src/assets/fonts/caveat-latin.woff2 +0 -0
  43. package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
  44. package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
  45. package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
  46. package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
  47. package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
  48. package/src/components/AdvancedSearch.vue +49 -0
  49. package/src/components/Breadcrumb.vue +5 -5
  50. package/src/components/ChoiceButtons.vue +152 -0
  51. package/src/components/CommandPalette.vue +5 -5
  52. package/src/components/ConnectionGuideView.vue +1 -1
  53. package/src/components/ConnectionNotice.vue +59 -0
  54. package/src/components/ConnectionsPanel.vue +30 -568
  55. package/src/components/ContextMenu.vue +31 -5
  56. package/src/components/DataTable.vue +1331 -0
  57. package/src/components/FilePane.vue +3 -0
  58. package/src/components/FilterBar.vue +12 -5
  59. package/src/components/GalleryView.vue +50 -2
  60. package/src/components/GridView.vue +51 -3
  61. package/src/components/HomeView.vue +18 -6
  62. package/src/components/InspectorPanel.vue +134 -17
  63. package/src/components/ListView.vue +366 -900
  64. package/src/components/LogoMark.vue +57 -0
  65. package/src/components/NFSExportsPanel.vue +76 -28
  66. package/src/components/OnboardingTour.vue +19 -7
  67. package/src/components/OperationsCenter.vue +77 -1
  68. package/src/components/PendingOpsTray.vue +59 -5
  69. package/src/components/PresenceBar.vue +7 -7
  70. package/src/components/ProductVersion.vue +31 -0
  71. package/src/components/QuickLook.vue +10 -14
  72. package/src/components/RecentlyOpened.vue +47 -17
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -56
  76. package/src/components/SSHKeysPanel.vue +79 -26
  77. package/src/components/SideNav.vue +260 -96
  78. package/src/components/StarButton.vue +1 -0
  79. package/src/components/StorageFields.vue +74 -6
  80. package/src/components/StorageTags.vue +62 -0
  81. package/src/components/TabBar.vue +1 -1
  82. package/src/components/TagKindIcon.vue +56 -0
  83. package/src/components/TagPicker.vue +226 -40
  84. package/src/components/ThemePalette.vue +4 -4
  85. package/src/components/TimeZonePicker.vue +29 -10
  86. package/src/components/TokensPanel.vue +82 -27
  87. package/src/components/Toolbar.vue +28 -6
  88. package/src/components/plugin/PluginConfirmModal.vue +54 -0
  89. package/src/components/plugin/PluginInspectorSection.vue +149 -0
  90. package/src/components/plugin/PluginPageView.vue +339 -0
  91. package/src/components/plugin/PluginViewModal.vue +159 -0
  92. package/src/components/plugin/SurfaceConversation.vue +76 -0
  93. package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
  94. package/src/components/plugin/SurfaceRenderer.vue +299 -0
  95. package/src/components/plugin/SurfaceSections.vue +91 -0
  96. package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
  97. package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
  98. package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
  99. package/src/components/plugin/nodes/SurfaceList.vue +127 -0
  100. package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
  101. package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
  102. package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
  103. package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
  104. package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
  105. package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
  106. package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
  107. package/src/components/public/PublicLanguagePicker.vue +58 -0
  108. package/src/components/public/PublicLinkPage.vue +286 -0
  109. package/src/components/public/PublicLinkPreview.vue +85 -0
  110. package/src/components/public/PublicPinGate.vue +108 -0
  111. package/src/components/public/PublicRequestBody.vue +204 -0
  112. package/src/components/public/PublicShareBody.vue +188 -0
  113. package/src/components/public/PublicShell.vue +332 -0
  114. package/src/composables/useConnections.ts +8 -2
  115. package/src/composables/useFileApi.ts +183 -44
  116. package/src/composables/useLocale.ts +277 -41
  117. package/src/composables/useNFSExports.ts +8 -10
  118. package/src/composables/useOperations.ts +44 -3
  119. package/src/composables/usePendingOps.ts +59 -8
  120. package/src/composables/usePluginActions.ts +121 -0
  121. package/src/composables/usePluginSurface.ts +223 -0
  122. package/src/composables/usePublicBranding.ts +172 -0
  123. package/src/composables/usePublicLink.ts +573 -0
  124. package/src/composables/usePublicPage.ts +61 -0
  125. package/src/composables/useS3Keys.ts +17 -12
  126. package/src/composables/useSSHKeys.ts +8 -10
  127. package/src/composables/useTokens.ts +8 -12
  128. package/src/composables/useUploadChunked.ts +18 -8
  129. package/src/composables/useViewerFetch.ts +6 -3
  130. package/src/index.ts +404 -8
  131. package/src/lib/actionIcons.ts +46 -1
  132. package/src/lib/advSearch.ts +14 -7
  133. package/src/lib/appLock.ts +130 -0
  134. package/src/lib/connection.ts +110 -0
  135. package/src/lib/destinationTree.ts +19 -3
  136. package/src/lib/direction.ts +353 -0
  137. package/src/lib/dragGhost.ts +6 -0
  138. package/src/lib/dropLimits.ts +69 -0
  139. package/src/lib/errorWords.ts +394 -0
  140. package/src/lib/externalReach.ts +5 -1
  141. package/src/lib/fileFilters.ts +7 -2
  142. package/src/lib/fileIcons.ts +4 -0
  143. package/src/lib/internalPaths.ts +77 -0
  144. package/src/lib/listInput.ts +64 -0
  145. package/src/lib/listing.ts +58 -23
  146. package/src/lib/modalStack.ts +26 -0
  147. package/src/lib/nodeRow.ts +121 -0
  148. package/src/lib/pdfFieldRules.ts +126 -0
  149. package/src/lib/pdfFields.ts +589 -0
  150. package/src/lib/pdfFieldsGeom.ts +203 -0
  151. package/src/lib/pdfjsLoader.ts +51 -0
  152. package/src/lib/personName.ts +47 -0
  153. package/src/lib/pluginApplies.ts +123 -0
  154. package/src/lib/pluginLabel.ts +72 -0
  155. package/src/lib/pluginMenu.ts +253 -0
  156. package/src/lib/pluginPage.ts +58 -0
  157. package/src/lib/plural.ts +181 -0
  158. package/src/lib/popupLayer.ts +103 -0
  159. package/src/lib/prefs.ts +563 -0
  160. package/src/lib/productVersion.ts +35 -0
  161. package/src/lib/publicLayout.ts +39 -0
  162. package/src/lib/serviceGate.ts +104 -0
  163. package/src/lib/shareTtl.ts +25 -11
  164. package/src/lib/signFonts.ts +85 -0
  165. package/src/lib/signaturePad.ts +111 -0
  166. package/src/lib/sortOrder.ts +100 -38
  167. package/src/lib/star.ts +7 -1
  168. package/src/lib/surfaceCell.ts +41 -0
  169. package/src/lib/surfaceConditions.ts +156 -0
  170. package/src/lib/surfaceOpen.ts +106 -0
  171. package/src/lib/surfaceSteps.ts +68 -0
  172. package/src/lib/surfaceValues.ts +191 -0
  173. package/src/lib/symlink.ts +150 -0
  174. package/src/lib/tableColumns.ts +493 -0
  175. package/src/lib/tableEnv.ts +34 -0
  176. package/src/lib/tablePrefs.ts +84 -0
  177. package/src/lib/tags.ts +121 -12
  178. package/src/lib/themes.ts +286 -18
  179. package/src/lib/tour.ts +152 -0
  180. package/src/lib/trashEmpty.ts +111 -0
  181. package/src/lib/trashTimeLeft.ts +23 -0
  182. package/src/lib/uiLocales.ts +460 -0
  183. package/src/lib/viewPrefs.ts +696 -660
  184. package/src/lib/viewPrefsHttp.ts +78 -0
  185. package/src/locales/en.ts +522 -119
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +539 -155
  188. package/src/modals/ConvertModal.vue +62 -11
  189. package/src/modals/DestinationPickerModal.vue +59 -16
  190. package/src/modals/Modal.vue +113 -23
  191. package/src/modals/NewDocumentModal.vue +16 -6
  192. package/src/modals/NewFolderModal.vue +8 -0
  193. package/src/modals/PermissionsModal.vue +272 -163
  194. package/src/modals/PreviewModal.vue +119 -20
  195. package/src/modals/RenameModal.vue +37 -1
  196. package/src/styles/base.css +2895 -241
  197. package/src/styles/sign-fonts.css +118 -0
  198. package/src/styles/variables.css +60 -0
  199. package/src/types/Connections.ts +17 -0
  200. package/src/types/ExplorerConfig.ts +117 -1
  201. package/src/types/FileNode.ts +58 -0
  202. package/src/types/Plugins.ts +494 -0
  203. package/src/types/Public.ts +212 -0
  204. package/src/viewers/ArchiveViewer.vue +93 -48
  205. package/src/viewers/CsvViewer.vue +99 -53
  206. package/src/viewers/DrawioViewer.vue +35 -9
  207. package/src/viewers/EpubViewer.vue +7 -8
  208. package/src/viewers/IpynbViewer.vue +3 -1
  209. package/src/viewers/MermaidViewer.vue +4 -5
  210. package/src/viewers/PdfViewer.vue +8 -17
  211. package/src/viewers/PsdViewer.vue +2 -4
  212. package/src/viewers/TiffViewer.vue +4 -5
  213. package/src/viewers/Viewer3D.vue +1 -1
  214. package/LICENSE +0 -21
  215. package/dist/ArchiveViewer-CLNevfPE.js +0 -124
  216. package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
  217. package/dist/CsvViewer-K8XA3z7N.js +0 -141
  218. package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
  219. package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
  220. package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
  221. package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
  222. package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
  223. package/dist/MermaidViewer-DqucH_io.js +0 -137
  224. package/dist/MermaidViewer-DqucH_io.js.map +0 -1
  225. package/dist/PsdViewer-CwiVENmq.js +0 -122
  226. package/dist/PsdViewer-CwiVENmq.js.map +0 -1
  227. package/dist/TiffViewer-CpewSVmQ.js +0 -142
  228. package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
  229. package/dist/Viewer3D-CIrOn63I.js.map +0 -1
  230. package/dist/index-Dx0IbBcI.js +0 -26831
  231. package/dist/index-Dx0IbBcI.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Public surfaces — what a visitor with a link, and no account, is told.
3
+ *
4
+ * ⚠⚠ ONE shell draws all of them (`components/public/PublicShell.vue`): a
5
+ * share (`/s/<token>`), a file request (`/d/<token>`) and an app plugin's
6
+ * page (a share whose `kind` is `app`). Before v3 the first two were HTML
7
+ * that Go wrote by hand and the third was a Vue screen, so the PIN box of a
8
+ * signature request looked nothing like the PIN box of a download and
9
+ * neither carried the instance's own colours. The shapes below are the
10
+ * seam: everything the shell needs to draw ANY of the three, and nothing
11
+ * that depends on which one it is.
12
+ *
13
+ * ⚠⚠ MIRROR, not a second design. Every field here exists because it exists
14
+ * in `backend/internal/api/handlers/public_api.go` under the same JSON name
15
+ * (`PublicBranding`, `PublicShare`, `PublicNode`, `PublicApp`,
16
+ * `PublicDrop`). When the Go side grows one, add it here with the same
17
+ * spelling; when the two disagree, the Go file is right.
18
+ */
19
+ import type { PluginText, PublicPageFile } from './Plugins';
20
+
21
+ /* ── branding ─────────────────────────────────────────────────────────── */
22
+
23
+ /**
24
+ * `GET /api/public/branding` — unauthenticated, cached.
25
+ *
26
+ * ⚠ Deliberately NOT the payload `/api/branding` serves: that one also
27
+ * carries the SSO button label, which a stranger has no business receiving.
28
+ *
29
+ * ⚠⚠ It used to carry the operator's custom stylesheet too, and that is the
30
+ * sentence that was wrong here for a release: the sheet left `/api/branding`
31
+ * for `GET /api/me/custom-css` behind auth, so no anonymous surface — this
32
+ * one included — can be handed a stylesheet that repaints a sign-in form.
33
+ */
34
+ export interface PublicBranding {
35
+ /** The instance's own name. Empty = the product's. */
36
+ name?: string;
37
+ logo_url?: string;
38
+ /** `#rgb` / `#rrggbb` — becomes `--fe-primary` on the public shell. */
39
+ accent?: string;
40
+ footer_text?: string;
41
+ hide_powered_by?: boolean;
42
+ /** The appearance a visitor gets before they choose: `system` | `light` | `dark`. */
43
+ theme?: 'system' | 'light' | 'dark' | string;
44
+ /** The instance's own default language. */
45
+ locale?: string;
46
+ /** The languages the public pages can be rendered in. */
47
+ locales?: string[];
48
+ /**
49
+ * The languages installed apps add (`manifest.ui_locales`): one row each —
50
+ * `code`, `source: "plugin"`, the `plugin` that brought it, `rtl` — and NO
51
+ * `strings`. One language's strings are `GET /api/public/ui-locales/{code}`
52
+ * (`lib/uiLocales.ensureLocaleStrings`), fetched when it is first used.
53
+ *
54
+ * ⚠⚠ Typed from the SERVER's bytes, not from belief: this field was a map
55
+ * on the wire and a list here for a whole release, and no pack's string
56
+ * ever reached a screen. web/tests/lib/uiLocales.test.ts reads
57
+ * backend/internal/api/handlers/testdata/wire/public-branding.json, which
58
+ * the Go handler's own test writes.
59
+ */
60
+ ui_locales?: PublicLocaleOption[];
61
+ }
62
+
63
+ /* ── the states every public link shares ──────────────────────────────── */
64
+
65
+ /** Why a link draws nothing, for a caller that logs it. */
66
+ export type PublicFailure = '' | 'not_found' | 'gone' | 'error';
67
+
68
+ /**
69
+ * What every public link answers, whatever kind it is.
70
+ *
71
+ * ⚠ `expired` and `revoked` are BOTH reported, and they are different facts:
72
+ * the clock ran out, versus the link is dead for another reason (its visit
73
+ * ceiling is spent, the app behind it was stopped, the file is gone). The
74
+ * shell shows one sentence for both — a visitor must not be told which —
75
+ * but the distinction is the server's to make, not the client's to lose.
76
+ *
77
+ * ⚠ `locked` is NOT one of them. It is the PIN gate shut after too many
78
+ * wrong answers, and it lifts by itself.
79
+ */
80
+ export interface PublicLinkBase {
81
+ needs_pin: boolean;
82
+ unlocked: boolean;
83
+ expired?: boolean;
84
+ revoked?: boolean;
85
+ locked?: boolean;
86
+ /** RFC 3339, or absent for "no expiry". */
87
+ expires_at?: string | null;
88
+ /** Words for the shell's header — a subject line, a file name. */
89
+ subject?: string;
90
+ }
91
+
92
+ /* ── a share: `/s/<token>` ────────────────────────────────────────────── */
93
+
94
+ /**
95
+ * `kind` decides the BODY the shell draws and nothing else — the header,
96
+ * the PIN gate, the language picker and the footer are the shell's.
97
+ *
98
+ * file → one document: download it
99
+ * folder → a folder, downloaded whole or walked
100
+ * app → an app plugin's screen, drawn by SurfaceRenderer
101
+ */
102
+ export type PublicShareKind = 'file' | 'folder' | 'app';
103
+
104
+ /**
105
+ * The little a visitor learns about the thing behind a link: what it is
106
+ * called, how big it is, what kind of thing it is. Never its path, never its
107
+ * storage, never who owns it. Absent until the link is unlocked.
108
+ */
109
+ export interface PublicNode {
110
+ name: string;
111
+ size?: number;
112
+ mime?: string;
113
+ }
114
+
115
+ /** The app surface a link carries, once it is unlocked. */
116
+ export interface PublicApp {
117
+ plugin: string;
118
+ page: string;
119
+ title?: PluginText;
120
+ /** Copies the app exposed to this visitor (`pub:N`). */
121
+ files?: PublicPageFile[];
122
+ }
123
+
124
+ /** One row of a folder share's listing, when the server sends one. */
125
+ export interface PublicEntry {
126
+ name: string;
127
+ /** Path relative to the share root, for the browse and download URLs. */
128
+ path: string;
129
+ is_dir: boolean;
130
+ size?: number;
131
+ mime?: string;
132
+ /** Epoch seconds or milliseconds, as the rest of the wire sends them. */
133
+ modified?: number;
134
+ /** Where the browser downloads this entry from; built from the token when absent. */
135
+ url?: string;
136
+ }
137
+
138
+ /** `GET /api/public/s/{token}`. */
139
+ export interface PublicShareInfo extends PublicLinkBase {
140
+ kind: PublicShareKind;
141
+ /** Visits left before the link stops answering; null = uncounted. */
142
+ visits_left?: number | null;
143
+ node?: PublicNode | null;
144
+ app?: PublicApp | null;
145
+ /**
146
+ * A folder share's listing.
147
+ *
148
+ * ⚠ Not sent today — the server answers a folder share with its NAME and
149
+ * leaves the walking to the no-JS page. Declared because the shell draws a
150
+ * listing the moment one arrives, and because the alternative (a second
151
+ * browse implementation in the SPA, reading a different endpoint) is the
152
+ * thing this round exists to stop.
153
+ */
154
+ entries?: PublicEntry[];
155
+ /** The path being listed, relative to the share root. */
156
+ path?: string;
157
+ }
158
+
159
+ /* ── a file request: `/d/<token>` ─────────────────────────────────────── */
160
+
161
+ /** What a file request will accept. */
162
+ export interface PublicDropLimits {
163
+ max_files?: number;
164
+ /** Megabytes, as the server states it. */
165
+ max_file_size_mb?: number;
166
+ /** Lower-case extensions, no dot. Empty = anything. */
167
+ allowed_ext?: string[];
168
+ /** The uploader is asked for their name. */
169
+ ask_name?: boolean;
170
+ }
171
+
172
+ /** `GET /api/public/d/{token}` — the drop box an outsider uploads into. */
173
+ export interface PublicRequestInfo extends PublicLinkBase {
174
+ kind?: 'drop' | string;
175
+ /** The destination's NAME — never its path, and never a listing. */
176
+ folder?: string;
177
+ /** How many files may still be dropped; null/absent = uncounted. */
178
+ uploads_left?: number | null;
179
+ limits?: PublicDropLimits;
180
+ }
181
+
182
+ /* ── languages ────────────────────────────────────────────────────────── */
183
+
184
+ /**
185
+ * One entry of the language list the interface offers.
186
+ *
187
+ * ⚠ The list is NOT a constant any more (v3 §5). It is the product's own
188
+ * languages plus whatever the server adds — including a language an app
189
+ * plugin ships (`manifest.ui_locales`), which is marked with the app it came
190
+ * from and leaves with it. A key the extra language does not carry falls
191
+ * back to English, exactly as a missing key always has.
192
+ */
193
+ export interface PublicLocaleOption {
194
+ /** BCP-47 primary subtag, e.g. `en`, `tr`, `ar`. */
195
+ code: string;
196
+ /** The language's name IN that language ("Türkçe"), not in the viewer's. */
197
+ label?: string;
198
+ /** `builtin` = shipped with filex; `plugin` = added by an app. */
199
+ source?: 'builtin' | 'plugin' | string;
200
+ /** The app that ships it, when `source` is `plugin` — shown beside the name. */
201
+ plugin?: string;
202
+ /**
203
+ * That language's strings by filex's own keys — absent on the branding
204
+ * list, present once `ensureLocaleStrings` has fetched them.
205
+ */
206
+ strings?: Record<string, string>;
207
+ /**
208
+ * Written right to left (Arabic, Hebrew, Persian, Urdu, …): the page's
209
+ * `dir` follows (lib/direction, docs/RTL.md).
210
+ */
211
+ rtl?: boolean;
212
+ }
@@ -12,6 +12,8 @@
12
12
  import { computed, onMounted, ref, watch } from 'vue';
13
13
  import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
14
14
  import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
15
+ import DataTable, { type DataColumn } from '../components/DataTable.vue';
16
+ import { useLocale } from '../composables/useLocale';
15
17
 
16
18
  interface ArchiveEntry {
17
19
  name: string;
@@ -25,6 +27,9 @@ const props = defineProps<{
25
27
  filePath?: string;
26
28
  ext: string;
27
29
  t?: (key: string, vars?: Record<string, string | number>) => string;
30
+ /** The interface language — the size column is written the way it writes
31
+ * numbers ("1,96 KB"), like every other size in the product. */
32
+ locale?: string;
28
33
  authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
29
34
  authCredentials?: RequestCredentials;
30
35
  /** ⚠ Where to ask. This used to be the literal '/api/files/archive/list',
@@ -46,11 +51,13 @@ function tt(key: string, fallback: string, vars?: Record<string, string | number
46
51
  return props.t ? props.t(key, vars) : fallback;
47
52
  }
48
53
 
54
+ /* ⚠ The product's one byte formatter (useLocale.formatSize). This file had
55
+ its own — base 1024, a dot, English units — so a zip's member read
56
+ "1.9 KB" beside the explorer's "1,96 KB" for the same bytes (QA,
57
+ 2026-09-21). */
58
+ const { formatSize } = useLocale(() => props.locale ?? 'en');
49
59
  function fmtSize(n: number): string {
50
- if (n < 1024) return `${n} B`;
51
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
52
- if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
53
- return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
60
+ return formatSize(n);
54
61
  }
55
62
 
56
63
  async function load(): Promise<void> {
@@ -76,20 +83,78 @@ async function load(): Promise<void> {
76
83
  body: JSON.stringify({ path: props.filePath }),
77
84
  });
78
85
  if (!res.ok) {
79
- throw new Error(`${res.status} ${res.statusText}`);
86
+ /* ⚠ Never the raw status. This printed "503 Service Unavailable" (or a
87
+ * JSON body) straight into the preview — the same failure the owner
88
+ * reported for ONLYOFFICE ("Config fetch 503: {…}"): a person is shown
89
+ * the HTTP layer instead of what happened. The server's reason, when it
90
+ * sent one we know, becomes a sentence; anything else is the generic
91
+ * "could not read the archive". */
92
+ throw new ArchiveError(await archiveErrorText(res));
80
93
  }
81
94
  const body = (await res.json()) as { entries?: ArchiveEntry[] };
82
95
  entries.value = body.entries ?? [];
83
96
  } catch (err) {
84
- error.value = err instanceof Error ? err.message : tt('viewer.archive.error', 'Could not read archive contents.');
97
+ error.value =
98
+ err instanceof ArchiveError ? err.message : tt('viewer.archive.error', 'Could not read archive contents.');
85
99
  } finally {
86
100
  loading.value = false;
87
101
  }
88
102
  }
89
103
 
104
+ /** A failure already worded for a person. Anything else that throws (the
105
+ * network, a JSON parse) reads as the generic sentence. */
106
+ class ArchiveError extends Error {}
107
+
108
+ async function archiveErrorText(res: Response): Promise<string> {
109
+ let code = '';
110
+ try {
111
+ const body = (await res.json()) as { error?: unknown; code?: unknown };
112
+ code = String(body?.code ?? body?.error ?? '');
113
+ } catch {
114
+ /* not JSON — the generic sentence below */
115
+ }
116
+ if (/not a zip|unsupported|not supported|format/i.test(code)) {
117
+ return tt('viewer.archive.unsupported', 'This kind of archive cannot be listed here. Download it to see what is inside.');
118
+ }
119
+ if (res.status === 413 || /too large|too big/i.test(code)) {
120
+ return tt('viewer.archive.too_large', 'This archive is too large to list here. Download it to see what is inside.');
121
+ }
122
+ return tt('viewer.archive.error', 'Could not read archive contents.');
123
+ }
124
+
90
125
  onMounted(load);
91
126
  watch(() => props.filePath, load);
92
127
 
128
+ /**
129
+ * The entries, in THE table (DataTable — the explorer's own). ⚠ It used to be
130
+ * the one table in the package allowed to be its own, with the reasoning that
131
+ * a file's contents answer to the viewer's layout. The owner's rule since
132
+ * 2026-09-21 has no such exception — "bir yere tablo gerekiyorsa bu tabloyu
133
+ * koymak zorundayız" — and an archive's listing is a file listing: it wants
134
+ * to be sorted by size and have its Name column widened exactly the way the
135
+ * folder it came from does.
136
+ */
137
+ const columns = computed<DataColumn<ArchiveEntry>[]>(() => [
138
+ {
139
+ id: 'name',
140
+ label: tt('viewer.name', 'Name'),
141
+ sortable: true,
142
+ width: 360,
143
+ class: 'filex-viewer-archive__name',
144
+ title: (e) => e.name,
145
+ },
146
+ {
147
+ id: 'size',
148
+ label: tt('viewer.size', 'Size'),
149
+ sortable: true,
150
+ width: 110,
151
+ align: 'right',
152
+ class: 'filex-viewer-archive__size',
153
+ format: (e) => (e.is_dir ? '' : fmtSize(e.size)),
154
+ sortValue: (e) => (e.is_dir ? -1 : e.size),
155
+ },
156
+ ]);
157
+
93
158
  const totalSize = computed(() => entries.value.reduce((sum, e) => sum + (e.size || 0), 0));
94
159
  const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
95
160
 
@@ -142,27 +207,22 @@ function entryTile(e: ArchiveEntry): string {
142
207
  {{ tt('viewer.archive.entries', `${fileCount} files`, { n: fileCount }) }}
143
208
  · {{ fmtSize(totalSize) }}
144
209
  </div>
145
- <table class="filex-viewer-archive__table">
146
- <thead>
147
- <tr>
148
- <th>{{ tt('viewer.name', 'Name') }}</th>
149
- <th class="filex-viewer-archive__size">{{ tt('viewer.size', 'Size') }}</th>
150
- </tr>
151
- </thead>
152
- <tbody>
153
- <tr v-for="(e, i) in entries" :key="i" :data-dir="e.is_dir ? '1' : '0'">
154
- <td>
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>
160
- {{ e.name }}
161
- </td>
162
- <td class="filex-viewer-archive__size">{{ e.is_dir ? '' : fmtSize(e.size) }}</td>
163
- </tr>
164
- </tbody>
165
- </table>
210
+ <DataTable
211
+ table-id="viewer.archive"
212
+ :columns="columns"
213
+ :rows="entries"
214
+ :row-key="(e: ArchiveEntry) => e.name"
215
+ :row-attrs="(e: ArchiveEntry) => ({ 'data-dir': e.is_dir ? '1' : '0' })"
216
+ >
217
+ <template #cell-name="{ row }">
218
+ <!-- ikon:emoji — the same tile the listing draws for the same kind
219
+ of file, so an archive's contents and the folder it came from
220
+ read as one product. -->
221
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
222
+ <span class="filex-viewer-archive__icon" aria-hidden="true" v-html="entryTile(row)"></span>
223
+ <span class="fe-list__cell-text">{{ row.name }}</span>
224
+ </template>
225
+ </DataTable>
166
226
  </div>
167
227
  </div>
168
228
  </template>
@@ -196,34 +256,19 @@ function entryTile(e: ArchiveEntry): string {
196
256
  margin-bottom: 12px;
197
257
  font-variant-numeric: tabular-nums;
198
258
  }
199
- .filex-viewer-archive__table {
200
- width: 100%;
201
- border-collapse: collapse;
202
- font-size: 13px;
203
- font-family: var(--fe-font-mono, monospace);
204
- }
205
- .filex-viewer-archive__table th,
206
- .filex-viewer-archive__table td {
207
- padding: 6px 12px;
208
- border-bottom: 1px solid var(--fe-border, #e2e6ed);
209
- text-align: left;
210
- }
211
- .filex-viewer-archive__table th {
212
- background: var(--fe-bg-elev, #f7f8fa);
213
- font-weight: 600;
214
- position: sticky;
215
- top: 0;
216
- }
259
+ /* The table itself is DataTable's (styles/base.css); only the viewer's own
260
+ touches stay here — tabular figures and muted sizes, a folder row read as
261
+ quieter than a file. */
217
262
  .filex-viewer-archive__size {
218
- text-align: right;
263
+ text-align: end;
219
264
  font-variant-numeric: tabular-nums;
220
265
  white-space: nowrap;
221
266
  color: var(--fe-text-muted, #5a6475);
222
267
  }
223
268
  .filex-viewer-archive__icon {
224
- margin-right: 6px;
269
+ margin-inline-end: 6px;
225
270
  }
226
- .filex-viewer-archive__table tr[data-dir="1"] {
271
+ .filex-viewer-archive .fe-list__row[data-dir="1"] {
227
272
  color: var(--fe-text-muted, #5a6475);
228
273
  }
229
274
  .filex-viewer-fallback {
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { sayFailure } from '../lib/errorWords';
2
3
  /**
3
4
  * CsvViewer — lightweight CSV / TSV table preview.
4
5
  *
@@ -17,6 +18,7 @@ import { computed, onMounted, ref, watch } from 'vue';
17
18
  import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
18
19
  import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
19
20
  import { fetchViewerText } from '../composables/useViewerFetch';
21
+ import DataTable, { type DataColumn } from '../components/DataTable.vue';
20
22
 
21
23
  const props = defineProps<{
22
24
  url: string;
@@ -86,7 +88,7 @@ async function load(): Promise<void> {
86
88
  credentials: props.authCredentials,
87
89
  });
88
90
  } catch (err) {
89
- error.value = err instanceof Error ? err.message : 'fetch failed';
91
+ error.value = sayFailure(err, tt('viewer.failed_to_load', 'Failed to load file'), { t: props.t }).text;
90
92
  loading.value = false;
91
93
  return;
92
94
  }
@@ -125,27 +127,93 @@ const dataRows = computed<string[][]>(() => {
125
127
  return firstRowHeader.value ? rows.value.slice(1) : rows.value;
126
128
  });
127
129
 
128
- const filtered = computed<string[][]>(() => {
130
+ /** A parsed row with the line it came from, so `#` keeps naming the row in
131
+ * the FILE after a sort re-orders the screen. */
132
+ interface CsvRow {
133
+ n: number;
134
+ cells: string[];
135
+ }
136
+
137
+ const filtered = computed<CsvRow[]>(() => {
129
138
  const q = filter.value.trim().toLowerCase();
130
- if (!q) return dataRows.value;
131
- return dataRows.value.filter((r) =>
132
- r.some((c) => (c || '').toLowerCase().includes(q)),
133
- );
139
+ const all = dataRows.value.map((cells, i) => ({ n: i + 1, cells }));
140
+ if (!q) return all;
141
+ return all.filter((r) => r.cells.some((c) => (c || '').toLowerCase().includes(q)));
134
142
  });
135
143
 
144
+ /**
145
+ * THE SORT, done here and handed to the table as CONTROLLED.
146
+ *
147
+ * ⚠⚠ Here and not in the table, because the table only ever holds one page
148
+ * (100 rows) of the parsed file, and a table that sorts the page it holds
149
+ * would call a re-ordered 100 of 1000 "sorted" — so it rightly closes its
150
+ * headers over a paged list. This component holds EVERY parsed row, so it can
151
+ * sort all of them and then page, which is the honest order.
152
+ */
153
+ const sort = ref<{ key: string; dir: 'asc' | 'desc' } | null>(null);
154
+
155
+ function cellOf(r: CsvRow, key: string): string {
156
+ if (key === 'n') return String(r.n);
157
+ return r.cells[Number(key.slice(1))] ?? '';
158
+ }
159
+
160
+ const sorted = computed<CsvRow[]>(() => {
161
+ const s = sort.value;
162
+ if (!s) return filtered.value;
163
+ const dir = s.dir === 'asc' ? 1 : -1;
164
+ return [...filtered.value].sort((a, b) => {
165
+ if (s.key === 'n') return dir * (a.n - b.n);
166
+ const x = cellOf(a, s.key);
167
+ const y = cellOf(b, s.key);
168
+ // Empty cells last in both directions; numbers as numbers.
169
+ if (!x !== !y) return x ? -1 : 1;
170
+ const nx = Number(x);
171
+ const ny = Number(y);
172
+ if (x && y && Number.isFinite(nx) && Number.isFinite(ny)) return dir * (nx - ny) || a.n - b.n;
173
+ return dir * x.localeCompare(y, undefined, { numeric: true, sensitivity: 'base' }) || a.n - b.n;
174
+ });
175
+ });
176
+
177
+ function onSort(next: { key: string; dir: 'asc' | 'desc' }) {
178
+ sort.value = next;
179
+ }
180
+
136
181
  const totalPages = computed(() =>
137
- Math.max(1, Math.ceil(filtered.value.length / PAGE_SIZE)),
182
+ Math.max(1, Math.ceil(sorted.value.length / PAGE_SIZE)),
138
183
  );
139
184
 
140
- const visibleRows = computed<string[][]>(() => {
185
+ const visibleRows = computed<CsvRow[]>(() => {
141
186
  const start = (page.value - 1) * PAGE_SIZE;
142
- return filtered.value.slice(start, start + PAGE_SIZE);
187
+ return sorted.value.slice(start, start + PAGE_SIZE);
143
188
  });
144
189
 
145
- watch(filter, () => {
190
+ watch([filter, sort], () => {
146
191
  page.value = 1;
147
192
  });
148
193
 
194
+ /** The file's columns, as THE table (DataTable — the explorer's own) draws
195
+ * them. `#` is the lead: it is what names a row of a spreadsheet. */
196
+ const columns = computed<DataColumn<CsvRow>[]>(() => [
197
+ {
198
+ id: 'n',
199
+ label: '#',
200
+ lead: true,
201
+ width: 64,
202
+ min: 48,
203
+ align: 'right',
204
+ sortable: true,
205
+ class: 'filex-viewer-csv__rownum',
206
+ format: (r) => r.n,
207
+ },
208
+ ...headers.value.map((h, i) => ({
209
+ id: `c${i}`,
210
+ label: h,
211
+ width: 140,
212
+ sortable: true,
213
+ format: (r: CsvRow) => r.cells[i] ?? '',
214
+ })),
215
+ ]);
216
+
149
217
  function tt(key: string, fallback: string, vars?: Record<string, string | number>): string {
150
218
  return props.t ? props.t(key, vars) : fallback;
151
219
  }
@@ -177,20 +245,19 @@ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ex
177
245
  ></span>
178
246
  <p>{{ tt('viewer.loading', 'Loading…') }}</p>
179
247
  </div>
180
- <table v-else class="filex-viewer-csv__table">
181
- <thead>
182
- <tr>
183
- <th class="filex-viewer-csv__rownum">#</th>
184
- <th v-for="(h, i) in headers" :key="i">{{ h }}</th>
185
- </tr>
186
- </thead>
187
- <tbody>
188
- <tr v-for="(row, ri) in visibleRows" :key="ri">
189
- <td class="filex-viewer-csv__rownum">{{ (page - 1) * 100 + ri + 1 }}</td>
190
- <td v-for="(c, ci) in row" :key="ci">{{ c }}</td>
191
- </tr>
192
- </tbody>
193
- </table>
248
+ <!-- ⚠ No table id: a spreadsheet's columns are whatever THIS file has
249
+ (`c0`, `c1`, …), so widths remembered against them would land on a
250
+ different file's different columns next time. The arrangement
251
+ lives for this preview. -->
252
+ <DataTable
253
+ v-else
254
+ :table-id="undefined"
255
+ :columns="columns"
256
+ :rows="visibleRows"
257
+ :row-key="(r: CsvRow) => r.n"
258
+ :sort="sort"
259
+ @sort="onSort"
260
+ />
194
261
  </div>
195
262
  <div v-if="!loading && !error && totalPages > 1" class="filex-viewer-csv__pager">
196
263
  <button
@@ -199,7 +266,7 @@ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ex
199
266
  :disabled="page <= 1"
200
267
  @click="page--"
201
268
  >‹</button>
202
- <span class="filex-viewer-csv__pageno">{{ page }} / {{ totalPages }} ({{ tt('viewer.csv_rows', `${filtered.length} rows`, { n: filtered.length }) }})</span>
269
+ <span class="filex-viewer-csv__pageno"><bdi dir="ltr">{{ page }} / {{ totalPages }}</bdi> ({{ tt('viewer.csv_rows', `${filtered.length} rows`, { n: filtered.length }) }})</span>
203
270
  <button
204
271
  type="button"
205
272
  class="filex-viewer-btn"
@@ -266,39 +333,18 @@ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ex
266
333
  flex: 1;
267
334
  overflow: auto;
268
335
  }
269
- .filex-viewer-csv__table {
270
- border-collapse: collapse;
271
- width: max-content;
272
- min-width: 100%;
273
- font-size: 12px;
336
+ .filex-viewer-csv__pane .fe-list__cell {
274
337
  font-family: var(--fe-font-mono, monospace);
338
+ font-size: 12px;
275
339
  }
276
- .filex-viewer-csv__table th,
277
- .filex-viewer-csv__table td {
278
- padding: 4px 10px;
279
- border: 1px solid var(--fe-border, #e2e6ed);
280
- white-space: nowrap;
281
- max-width: 320px;
282
- overflow: hidden;
283
- text-overflow: ellipsis;
284
- }
285
- .filex-viewer-csv__table th {
286
- background: var(--fe-bg-elev, #f7f8fa);
287
- position: sticky;
288
- top: 0;
289
- font-weight: 600;
290
- text-align: left;
291
- }
292
- .filex-viewer-csv__table tbody tr:nth-child(even) {
293
- background: var(--fe-bg-hover, rgba(0, 0, 0, 0.02));
294
- }
340
+ /* The table itself is DataTable's (styles/base.css) — the product's one table.
341
+ ⚠ The row number used to pin itself (`position: sticky; left: 0`) because
342
+ this was a table of its own; it is the table's LEAD now, and the table
343
+ freezes its lead itself, only while there is room to (a pin written here
344
+ would fight that gate). What stays is the look of a row number. */
295
345
  .filex-viewer-csv__rownum {
296
346
  color: var(--fe-text-muted, #5a6475);
297
- text-align: right;
298
- background: var(--fe-bg-elev, #f7f8fa);
299
347
  font-variant-numeric: tabular-nums;
300
- position: sticky;
301
- left: 0;
302
348
  }
303
349
  .filex-viewer-csv__pager {
304
350
  display: flex;