@brftech/filex-core 0.42.2 → 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 (234) 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-CC6rJXeP.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-CAoTSXTO.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-CdZeK4j4.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 +5227 -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 +1055 -253
  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 +31 -15
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -62
  76. package/src/components/SSHKeysPanel.vue +79 -30
  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 -31
  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 +18 -3
  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 +515 -117
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +536 -152
  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 +2890 -260
  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 +50 -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-BUQWLhwX.js +0 -124
  216. package/dist/ArchiveViewer-BUQWLhwX.js.map +0 -1
  217. package/dist/CsvViewer-Bg2_aqzc.js +0 -141
  218. package/dist/CsvViewer-Bg2_aqzc.js.map +0 -1
  219. package/dist/DrawioViewer-CC6rJXeP.js.map +0 -1
  220. package/dist/EpubViewer-CAoTSXTO.js.map +0 -1
  221. package/dist/IpynbViewer-D0GwGpnE.js +0 -184
  222. package/dist/IpynbViewer-D0GwGpnE.js.map +0 -1
  223. package/dist/MermaidViewer-DGwAvRZg.js +0 -137
  224. package/dist/MermaidViewer-DGwAvRZg.js.map +0 -1
  225. package/dist/PsdViewer-DhGweA0m.js +0 -122
  226. package/dist/PsdViewer-DhGweA0m.js.map +0 -1
  227. package/dist/TiffViewer-D0AurvAh.js +0 -142
  228. package/dist/TiffViewer-D0AurvAh.js.map +0 -1
  229. package/dist/Viewer3D-CdZeK4j4.js.map +0 -1
  230. package/dist/index-BArI-B1M.js +0 -26911
  231. package/dist/index-BArI-B1M.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
  234. package/src/composables/useScrolledX.ts +0 -21
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ // The filex mark — the ONE copy of it in the product's own code.
3
+ //
4
+ // A tiny folder + checkmark: recognisable as a file manager, inline SVG so it
5
+ // needs no asset pipeline. `class` falls through to the root element, so a
6
+ // caller sizes it.
7
+ //
8
+ // ⚠⚠ It lives in `packages/core` and not in `web/` because the public shell
9
+ // (`components/public/PublicShell.vue`) draws it too, and `packages/core`
10
+ // cannot import out of the web app. `web/src/components/LogoMark.vue` is a
11
+ // re-export of this file; every existing call site keeps working.
12
+ // `scripts/dup-scan.mjs` names THIS path as the concept's home
13
+ // (`brand-mark`), and `web/tests/quality/duplication.test.ts` fails on a
14
+ // second hand-typed copy — for a reason with a date on it: the public share
15
+ // page carried the pre-rebrand indigo for a whole release because its mark
16
+ // was re-typed in Go rather than generated from here.
17
+ //
18
+ // gorunum:v1 — flat product blue, not the old indigo→violet gradient ("maviye
19
+ // çekelim"). The fill is the palette's own `--fe-primary`, so the mark is
20
+ // #2f6ceb on a light page and #5b8cff on a dark one, and it follows any theme
21
+ // the product ever ships rather than being a colour no theme can reach. On a
22
+ // public link with an instance accent set the same token carries the
23
+ // operator's colour (`accentStyleOf`), which is the behaviour a brandable
24
+ // mark should have.
25
+ //
26
+ // ⚠ The literal is a floor, not the value in use: `fill` has no inherited
27
+ // value to fall back to, so an unstyled context (core's stylesheet missing)
28
+ // would paint the mark BLACK rather than blue. Measured in the browser on
29
+ // /admin/login, /admin/dashboard and /admin/about: the computed fill is the
30
+ // token in all three, in both variants — the literal never renders today.
31
+ //
32
+ // The same blue is written out by hand in three static files that have no
33
+ // token scope at all: web/public/favicon.svg, web/public/icons/icon.svg and
34
+ // the `theme-color` meta in web/index.html (plus the PWA manifest's
35
+ // theme_color in web/vite.config.ts). Change one, change all four.
36
+ </script>
37
+
38
+ <template>
39
+ <svg viewBox="0 0 32 32" aria-hidden="true">
40
+ <rect width="32" height="32" rx="7" fill="var(--fe-primary, #2f6ceb)" />
41
+ <path
42
+ d="M7 11a2 2 0 0 1 2-2h5l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V11z"
43
+ fill="none"
44
+ stroke="#fff"
45
+ stroke-width="1.8"
46
+ stroke-linejoin="round"
47
+ />
48
+ <path
49
+ d="M11.5 17.5l3 2.5 5.5-6"
50
+ fill="none"
51
+ stroke="#fff"
52
+ stroke-width="1.8"
53
+ stroke-linecap="round"
54
+ stroke-linejoin="round"
55
+ />
56
+ </svg>
57
+ </template>
@@ -16,6 +16,8 @@ import { computed, onMounted, ref } from 'vue';
16
16
  import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
17
17
  import type { NFSExport } from '../types/NFSExports';
18
18
  import { useLocale } from '../composables/useLocale';
19
+ import DataTable, { type DataColumn } from './DataTable.vue';
20
+ import type { ContextAction } from './ContextMenu.vue';
19
21
  import { useNFSExports } from '../composables/useNFSExports';
20
22
  import { resolveLocale } from '../locales/resolve';
21
23
 
@@ -94,6 +96,61 @@ async function mint() {
94
96
  }
95
97
  }
96
98
 
99
+ /** The row's verbs, behind its one `Actions` control. ⚠ `revoke` keeps the
100
+ * two-step confirmation it had as a loose button. */
101
+ function rowActions(e: NFSExport): ContextAction[] {
102
+ /* The whole control used to be `:disabled="busy"`; the table draws the
103
+ control now, so each verb carries it. */
104
+ return [
105
+ {
106
+ key: 'toggle',
107
+ label: e.disabled_at ? t('conn.nfs.enable') : t('conn.nfs.disable'),
108
+ icon: e.disabled_at ? 'check' : 'lock',
109
+ disabled: busy.value,
110
+ },
111
+ {
112
+ key: 'revoke',
113
+ label: confirmRemove.value === e.id ? t('conn.nfs.confirm') : t('conn.nfs.revoke'),
114
+ icon: 'delete',
115
+ danger: true,
116
+ disabled: busy.value,
117
+ },
118
+ ];
119
+ }
120
+
121
+ /** The list's columns — the product's one table (DataTable). */
122
+ const columns = computed<DataColumn<NFSExport>[]>(() => [
123
+ {
124
+ id: 'label',
125
+ label: t('conn.nfs.col.label'),
126
+ sortable: true,
127
+ width: 180,
128
+ format: (e) => e.label || t('conn.nfs.noLabel'),
129
+ },
130
+ { id: 'scope', label: t('conn.nfs.col.scope'), sortable: true, width: 180, format: scopeOf },
131
+ {
132
+ id: 'mode',
133
+ label: t('conn.nfs.col.mode'),
134
+ sortable: true,
135
+ width: 120,
136
+ format: (e) => (e.read_only ? t('conn.nfs.modeRead') : t('conn.nfs.modeWrite')),
137
+ },
138
+ {
139
+ id: 'lastUsed',
140
+ label: t('conn.nfs.col.lastUsed'),
141
+ sortable: true,
142
+ sortDir: 'desc',
143
+ width: 140,
144
+ format: (e) => (e.last_used_at ? shortDate(e.last_used_at) : t('conn.nfs.neverUsed')),
145
+ sortValue: (e) => (e.last_used_at ? new Date(e.last_used_at).getTime() : null),
146
+ },
147
+ ]);
148
+
149
+ function onRowAction(key: string, e: NFSExport) {
150
+ if (key === 'toggle') void toggle(e);
151
+ else if (key === 'revoke') void drop(e);
152
+ }
153
+
97
154
  async function toggle(e: NFSExport) {
98
155
  busy.value = true;
99
156
  try {
@@ -234,35 +291,26 @@ function scopeOf(e: NFSExport): string {
234
291
  </button>
235
292
  </div>
236
293
 
294
+ <!-- ⚠ THE table (DataTable — the explorer's own), not a table of its own:
295
+ reachable from the admin panel's Connections menu and from the
296
+ explorer, so it resizes, sorts, hides and moves columns and remembers
297
+ that on the account (`conn.nfs`), like every other table. (It once had
298
+ no scroll container at all, so its actions cell could not pin; the
299
+ one table owns that now.) -->
237
300
  <p v-if="loading" class="fe-s3keys__muted">…</p>
238
- <table v-else-if="exports.length" class="fe-s3keys__table">
239
- <thead>
240
- <tr>
241
- <th>{{ t('conn.nfs.col.label') }}</th>
242
- <th>{{ t('conn.nfs.col.scope') }}</th>
243
- <th>{{ t('conn.nfs.col.mode') }}</th>
244
- <th>{{ t('conn.nfs.col.lastUsed') }}</th>
245
- <th></th>
246
- </tr>
247
- </thead>
248
- <tbody>
249
- <tr v-for="e in exports" :key="e.id" :class="{ 'is-off': !!e.disabled_at }">
250
- <td>{{ e.label || t('conn.nfs.noLabel') }}</td>
251
- <td>{{ scopeOf(e) }}</td>
252
- <td>{{ e.read_only ? t('conn.nfs.modeRead') : t('conn.nfs.modeWrite') }}</td>
253
- <td>{{ e.last_used_at ? shortDate(e.last_used_at) : t('conn.nfs.neverUsed') }}</td>
254
- <td class="fe-s3keys__actions">
255
- <button class="fe-s3keys__link" :disabled="busy" @click="toggle(e)">
256
- {{ e.disabled_at ? t('conn.nfs.enable') : t('conn.nfs.disable') }}
257
- </button>
258
- <button class="fe-s3keys__link is-danger" :disabled="busy" @click="drop(e)">
259
- {{ confirmRemove === e.id ? t('conn.nfs.confirm') : t('conn.nfs.revoke') }}
260
- </button>
261
- </td>
262
- </tr>
263
- </tbody>
264
- </table>
265
- <p v-else-if="canMint" class="fe-s3keys__muted">{{ t('conn.nfs.empty') }}</p>
301
+ <DataTable
302
+ v-else
303
+ table-id="conn.nfs"
304
+ :columns="columns"
305
+ :rows="exports"
306
+ row-key="id"
307
+ :locale="locale"
308
+ :empty="t('conn.nfs.empty')"
309
+ :row-class="(e: NFSExport) => (e.disabled_at ? 'is-muted' : undefined)"
310
+ :row-actions="rowActions"
311
+ :row-actions-test-id="(e: NFSExport) => `nfs-export-actions-${e.id}`"
312
+ @row-action="(key: string, e: NFSExport) => onRowAction(key, e)"
313
+ />
266
314
  </section>
267
315
  </template>
268
316
 
@@ -19,6 +19,7 @@
19
19
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
20
20
  import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
21
21
  import { useLocale } from '../composables/useLocale';
22
+ import { inlineKeyStep } from '../lib/direction';
22
23
  import { shortcutHint } from '../composables/useKeyboardShortcuts';
23
24
 
24
25
  const props = defineProps<{
@@ -33,7 +34,8 @@ const emit = defineEmits<{
33
34
  (e: 'close'): void;
34
35
  }>();
35
36
 
36
- const { t } = useLocale(() => props.locale);
37
+ // ⚠ RTL: `dir` goes on the teleported tour and orders its choices below.
38
+ const { t, dir } = useLocale(() => props.locale);
37
39
 
38
40
  /* The tour teaches two keys by name, and both are remappable. Read them
39
41
  * from the registry so the sentence stays true for whoever is taking the
@@ -260,6 +262,14 @@ async function place() {
260
262
  const midX = clampX(rect.left + rect.width / 2 - cw / 2);
261
263
  const midY = clampY(rect.top + rect.height / 2 - ch / 2);
262
264
 
265
+ /* Beside it: toward the inline END first — right in LTR, LEFT in RTL, where
266
+ the reading eye goes next. ⚠ The spotlight and these numbers stay
267
+ PHYSICAL (they are the target's own viewport box); only which side is
268
+ tried first follows the direction. */
269
+ const toRight = { fits: sRight + GAP + cw <= vw - 12, left: sRight + GAP };
270
+ const toLeft = { fits: sLeft - GAP - cw >= 12, left: sLeft - GAP - cw };
271
+ const beside = dir.value === 'rtl' ? [toLeft, toRight] : [toRight, toLeft];
272
+
263
273
  let top: number;
264
274
  let left: number;
265
275
  if (sBottom + GAP + ch <= vh - 12) {
@@ -268,11 +278,11 @@ async function place() {
268
278
  } else if (sTop - GAP - ch >= 12) {
269
279
  top = sTop - GAP - ch;
270
280
  left = midX;
271
- } else if (sRight + GAP + cw <= vw - 12) {
272
- left = sRight + GAP;
281
+ } else if (beside[0].fits) {
282
+ left = beside[0].left;
273
283
  top = midY;
274
- } else if (sLeft - GAP - cw >= 12) {
275
- left = sLeft - GAP - cw;
284
+ } else if (beside[1].fits) {
285
+ left = beside[1].left;
276
286
  top = midY;
277
287
  } else {
278
288
  // Nothing fits — a target that fills the viewport. Clamp, and accept the
@@ -338,10 +348,11 @@ function onKey(e: KeyboardEvent) {
338
348
  if (e.key === 'Escape') {
339
349
  e.stopPropagation();
340
350
  skip();
341
- } else if (e.key === 'ArrowRight' || e.key === 'Enter') {
351
+ } else if (e.key === 'Enter' || inlineKeyStep(e.key, dir.value) === 1) {
352
+ // ⚠ RTL: "forward" is the arrow that points the way the line reads — ←.
342
353
  e.preventDefault();
343
354
  next();
344
- } else if (e.key === 'ArrowLeft') {
355
+ } else if (inlineKeyStep(e.key, dir.value) === -1) {
345
356
  e.preventDefault();
346
357
  back();
347
358
  }
@@ -399,6 +410,7 @@ const themeClass = computed(() => `fe-ctx-backdrop--theme-${props.theme || 'auto
399
410
  <div
400
411
  v-if="open && step"
401
412
  class="fe-tour"
413
+ :dir="dir"
402
414
  :class="themeClass"
403
415
  :data-prefers-dark="prefersDark ? '1' : '0'"
404
416
  role="dialog"
@@ -65,6 +65,11 @@ const KIND_ICONS: Record<Operation['kind'], string> = {
65
65
  delete: 'M5 7h14M9 7V5h6v2M8 7l1 13h6l1-13',
66
66
  convert: 'M20 8A8 8 0 0 0 6 6L4 8M4 16a8 8 0 0 0 14 2l2-2M20 3v5h-5M4 21v-5h5',
67
67
  archive: 'M4 8V5h16v3zM5 8h14v12H5zM10 12h4',
68
+ /* "Empty the trash" — the bin, with the lines of what is going. */
69
+ trash: 'M5 7h14M9 7V5h6v2M8 7l1 13h6l1-13M10.5 11v5M13.5 11v5',
70
+ /* An app-plugin job — the puzzle piece its menu row wears (lib/actionIcons). */
71
+ plugin:
72
+ 'M9.5 4.5a2 2 0 1 1 4 0h3a1.5 1.5 0 0 1 1.5 1.5v3a2 2 0 1 1 0 4v3a1.5 1.5 0 0 1-1.5 1.5h-3a2 2 0 1 1-4 0h-3A1.5 1.5 0 0 1 5 16v-3a2 2 0 1 1 0-4V6a1.5 1.5 0 0 1 1.5-1.5z',
68
73
  };
69
74
 
70
75
  function kindLabel(o: Operation): string {
@@ -75,9 +80,30 @@ function rowTitle(o: Operation): string {
75
80
  return o.name || kindLabel(o);
76
81
  }
77
82
 
83
+ /**
84
+ * The words for an app job's output mode, or `''`. ⚠ Only the two WRITING
85
+ * modes get a chip: `none` is the normal case for a job that reports rather
86
+ * than produces (a probe, a notification), and labelling it "no output"
87
+ * would put a badge on almost every row for no information.
88
+ */
89
+ function outputModeLabel(o: Operation): string {
90
+ if (o.outputMode === 'sibling') return t('plugin.output.sibling');
91
+ if (o.outputMode === 'version') return t('plugin.output.version');
92
+ return '';
93
+ }
94
+
95
+ function outputModeTitle(o: Operation): string {
96
+ if (o.outputMode === 'sibling') return t('plugin.output.sibling_title');
97
+ if (o.outputMode === 'version') return t('plugin.output.version_title');
98
+ return '';
99
+ }
100
+
78
101
  function statusText(o: Operation): string {
79
102
  if (o.status === 'running') {
80
103
  if (o.queued) return t('opc.queued');
104
+ // A plugin job says what it is doing in its own words (`job_progress`
105
+ // message); that beats a bare count.
106
+ if (o.message) return o.message;
81
107
  if (o.totalBytes && o.totalBytes > 0) {
82
108
  return `${formatSize(o.uploadedBytes ?? 0)} / ${formatSize(o.totalBytes)}`;
83
109
  }
@@ -86,11 +112,16 @@ function statusText(o: Operation): string {
86
112
  }
87
113
  return t('opc.status.running');
88
114
  }
89
- if (o.status === 'done') return t('opc.status.done');
115
+ if (o.status === 'done') return o.message || t('opc.status.done');
90
116
  if (o.status === 'aborted') return t('opc.status.aborted');
91
117
  return o.error || t('opc.status.error');
92
118
  }
93
119
 
120
+ /** A finished job that produced files can be opened from its row. */
121
+ function canOpen(o: Operation): boolean {
122
+ return o.status === 'done' && o.outputs.length > 0;
123
+ }
124
+
94
125
  function chipText(o: Operation): string {
95
126
  return t(`opc.status.${o.status === 'running' ? 'running' : o.status}`);
96
127
  }
@@ -159,9 +190,24 @@ onBeforeUnmount(() => {
159
190
  <div class="fe-opc__name" :title="rowTitle(o)">{{ rowTitle(o) }}</div>
160
191
  <div class="fe-opc__sub">
161
192
  <span class="fe-opc__kind">{{ kindLabel(o) }}</span>
193
+ <template v-if="outputModeLabel(o)">
194
+ <span class="fe-opc__dot" aria-hidden="true">·</span>
195
+ <span class="fe-opc__outmode" :title="outputModeTitle(o)" data-testid="opc-output-mode">
196
+ {{ outputModeLabel(o) }}
197
+ </span>
198
+ </template>
162
199
  <span class="fe-opc__dot" aria-hidden="true">·</span>
163
200
  <span class="fe-opc__state">{{ statusText(o) }}</span>
164
201
  </div>
202
+ <!-- An administrator's second line (lib/errorWords): the raw
203
+ words behind the sentence above. Nobody else gets them. -->
204
+ <div
205
+ v-if="o.status === 'error' && o.errorDetail"
206
+ class="fe-opc__sub fe-opc__sub--detail"
207
+ data-testid="opc-error-detail"
208
+ >
209
+ {{ o.errorDetail }}
210
+ </div>
165
211
  <div
166
212
  v-if="o.status === 'running' && o.percent !== null"
167
213
  class="fe-opc__bar"
@@ -185,6 +231,13 @@ onBeforeUnmount(() => {
185
231
  class="fe-opc__act"
186
232
  @click="center.cancel(o.key)"
187
233
  >{{ t('opc.cancel') }}</button>
234
+ <button
235
+ v-if="canOpen(o)"
236
+ type="button"
237
+ class="fe-opc__act"
238
+ data-testid="opc-open"
239
+ @click="center.open(o.key)"
240
+ >{{ t('opc.open') }}</button>
188
241
  <button
189
242
  v-if="o.status === 'error' && o.retryable"
190
243
  type="button"
@@ -232,10 +285,33 @@ onBeforeUnmount(() => {
232
285
  </svg>
233
286
  <div class="fe-opc__mid">
234
287
  <div class="fe-opc__name" :title="rowTitle(o)">{{ rowTitle(o) }}</div>
288
+ <!-- ⚠ The history row keeps the chip: in `version` mode
289
+ "Open" lands on the file the job started from, and
290
+ without these words that reads as "it did nothing". -->
291
+ <div v-if="outputModeLabel(o)" class="fe-opc__sub">
292
+ <span class="fe-opc__outmode" :title="outputModeTitle(o)" data-testid="opc-output-mode">
293
+ {{ outputModeLabel(o) }}
294
+ </span>
295
+ </div>
235
296
  <div v-if="o.status === 'error' && o.error" class="fe-opc__sub fe-opc__sub--err">
236
297
  {{ o.error }}
237
298
  </div>
299
+ <!-- An administrator's second line: the raw words behind the
300
+ sentence (lib/errorWords). Nobody else is given them. -->
301
+ <div
302
+ v-if="o.status === 'error' && o.errorDetail"
303
+ class="fe-opc__sub fe-opc__sub--detail"
304
+ data-testid="opc-error-detail"
305
+ >
306
+ {{ o.errorDetail }}
307
+ </div>
238
308
  </div>
309
+ <button
310
+ v-if="canOpen(o)"
311
+ type="button"
312
+ class="fe-opc__act"
313
+ @click="center.open(o.key)"
314
+ >{{ t('opc.open') }}</button>
239
315
  <span class="fe-opc__chip" :class="`is-${o.status}`">{{ chipText(o) }}</span>
240
316
  </li>
241
317
  </ul>
@@ -16,26 +16,73 @@
16
16
  import { watch } from 'vue';
17
17
  import type { LocaleCode } from '../types/ExplorerConfig';
18
18
  import type { PendingOp } from '../composables/usePendingOps';
19
- import type { OperationsStore, OperationStatus } from '../composables/useOperations';
19
+ import type { OperationKind, OperationsStore, OperationStatus } from '../composables/useOperations';
20
20
  import { opPercent } from '../lib/opProgress';
21
+ import { opFailure } from '../lib/errorWords';
22
+ import { useLocale } from '../composables/useLocale';
21
23
 
22
24
  const props = defineProps<{
23
25
  ops: PendingOp[];
24
26
  /** Kept for mount compatibility — strings now render in OperationsCenter. */
25
27
  locale: LocaleCode;
26
28
  center: OperationsStore;
29
+ /**
30
+ * An app-plugin job's output mode, as the host resolves it (the explorer
31
+ * reads the cached actions list). ⚠ A resolver rather than a field on the
32
+ * row: the ops endpoint does not carry it, so only a caller that holds the
33
+ * actions list can answer — and one that does not simply draws no chip.
34
+ */
35
+ outputModeOf?: (op: PendingOp) => string | undefined;
36
+ /** May the viewer administer this instance? Then a failure's raw words are
37
+ * shown as a second line (lib/errorWords); nobody else ever sees them. */
38
+ callerAdmin?: boolean;
27
39
  }>();
28
40
 
41
+ const { t } = useLocale(() => props.locale);
42
+
43
+ /** A failed op, said — lib/errorWords `opFailure`, the same call the
44
+ * explorer's toast makes. */
45
+ function failureOf(op: PendingOp) {
46
+ return opFailure(op, t, { callerAdmin: props.callerAdmin === true });
47
+ }
48
+
29
49
  const emit = defineEmits<{
30
50
  (e: 'dismiss', id: number): void;
51
+ /** A plugin job's Cancel — the explorer posts `opsCancel`. */
52
+ (e: 'cancel', id: number): void;
53
+ /** "Open" on a finished job: reveal this output path in the listing. */
54
+ (e: 'open', id: number, path: string): void;
31
55
  }>();
32
56
 
33
57
  function mapStatus(op: PendingOp): OperationStatus {
34
58
  if (op.status === 'done') return 'done';
35
59
  if (op.status === 'error') return 'error';
60
+ if (op.status === 'cancelled') return 'aborted';
36
61
  return 'running'; // pending | running
37
62
  }
38
63
 
64
+ const DRAWN_KINDS: ReadonlySet<string> = new Set(['copy', 'move', 'delete', 'plugin']);
65
+
66
+ /** A queue kind the center can draw. Anything it has no glyph for is shown as
67
+ * a generic job (`plugin`), never folded into delete. */
68
+ function mapKind(op: PendingOp): OperationKind {
69
+ if (op.op_type === 'trash-empty') return 'trash';
70
+ return (DRAWN_KINDS.has(op.op_type) ? op.op_type : 'plugin') as OperationKind;
71
+ }
72
+
73
+ function isPluginJob(op: PendingOp): boolean {
74
+ return op.op_type === 'plugin';
75
+ }
76
+
77
+ /** Who may stop a row: a plugin job's starter (the server answers anyone
78
+ * else 403), and an "empty the trash" — which only an administrator
79
+ * starts — for an administrator. */
80
+ function mayCancel(op: PendingOp): boolean {
81
+ if (op.status !== 'pending' && op.status !== 'running') return false;
82
+ if (isPluginJob(op)) return true;
83
+ return op.op_type === 'trash-empty' && props.callerAdmin === true;
84
+ }
85
+
39
86
  /* issue #27 — bytes when a cross-storage transfer reports them, no fake 0%
40
87
  * otherwise; the rule is shared with the admin tray (lib/opProgress). */
41
88
  const percentOf = (op: PendingOp): number | null => opPercent(op);
@@ -48,19 +95,26 @@ watch(
48
95
  ops.map((op) => ({
49
96
  input: {
50
97
  id: op.id,
51
- kind: op.op_type,
52
- name: op.target_path || op.source_dir || '',
98
+ kind: mapKind(op),
99
+ /* A trash empty names no file: its kind is its title. */
100
+ name: op.op_type === 'trash-empty' ? '' : op.label || op.target_path || op.source_dir || '',
53
101
  percent: percentOf(op),
54
102
  status: mapStatus(op),
55
- error: op.error_message,
103
+ error: op.status === 'error' ? failureOf(op).text : null,
104
+ errorDetail: op.status === 'error' ? (failureOf(op).detail ?? null) : null,
56
105
  queued: op.status === 'pending',
57
106
  doneCount: op.progress_done,
58
107
  totalCount: op.progress_total,
59
- cancellable: false,
108
+ cancellable: mayCancel(op),
60
109
  retryable: false,
110
+ message: op.message ?? null,
111
+ outputs: (op.outputs ?? []).map((o) => o.path),
112
+ outputMode: (isPluginJob(op) ? props.outputModeOf?.(op) : undefined) ?? null,
61
113
  },
62
114
  actions: {
63
115
  dismiss: () => emit('dismiss', op.id),
116
+ cancel: () => emit('cancel', op.id),
117
+ open: (path: string) => emit('open', op.id, path),
64
118
  },
65
119
  })),
66
120
  );
@@ -71,7 +71,7 @@ function label(u: PresenceUser): string {
71
71
  :title="label(u)"
72
72
  >
73
73
  <PresenceAvatar :user="u" />
74
- <span class="fx-presence-chip-name">{{ u.name }}</span>
74
+ <span class="fx-presence-chip-name"><bdi>{{ u.name }}</bdi></span>
75
75
  <span v-if="u.file" class="fx-presence-chip-file">· {{ u.file }}</span>
76
76
  </span>
77
77
  </div>
@@ -131,7 +131,7 @@ function label(u: PresenceUser): string {
131
131
  justify-content: center;
132
132
  width: 1.5rem;
133
133
  height: 1.5rem;
134
- margin-left: -0.4rem;
134
+ margin-inline-start: -0.4rem;
135
135
  border-radius: 9999px;
136
136
  border: 2px solid var(--fx-presence-ring, #ffffff);
137
137
  color: #fff;
@@ -141,7 +141,7 @@ function label(u: PresenceUser): string {
141
141
  user-select: none;
142
142
  }
143
143
  .fx-presence-avatar:first-child {
144
- margin-left: 0;
144
+ margin-inline-start: 0;
145
145
  }
146
146
  /* A photo fills the circle; the identity colour stays as the ring backdrop so
147
147
  a picture with transparency still reads as that person. */
@@ -157,7 +157,7 @@ function label(u: PresenceUser): string {
157
157
  }
158
158
  .fx-presence-dot {
159
159
  position: absolute;
160
- right: -1px;
160
+ inset-inline-end: -1px;
161
161
  bottom: -1px;
162
162
  width: 0.5rem;
163
163
  height: 0.5rem;
@@ -166,7 +166,7 @@ function label(u: PresenceUser): string {
166
166
  border: 1.5px solid var(--fx-presence-ring, #ffffff);
167
167
  }
168
168
  .fx-presence-more {
169
- margin-left: 0.15rem;
169
+ margin-inline-start: 0.15rem;
170
170
  font-size: 0.7rem;
171
171
  color: rgb(113 113 122); /* zinc-500 */
172
172
  }
@@ -188,7 +188,7 @@ function label(u: PresenceUser): string {
188
188
  align-items: center;
189
189
  gap: 0.35rem;
190
190
  flex: 0 0 auto;
191
- padding: 0.15rem 0.5rem 0.15rem 0.2rem;
191
+ padding-block: 0.15rem; padding-inline: 0.2rem 0.5rem;
192
192
  border-radius: 9999px;
193
193
  background: rgb(244 244 245); /* zinc-100 */
194
194
  font-size: 0.75rem;
@@ -196,7 +196,7 @@ function label(u: PresenceUser): string {
196
196
  white-space: nowrap;
197
197
  }
198
198
  .fx-presence-chip .fx-presence-avatar {
199
- margin-left: 0;
199
+ margin-inline-start: 0;
200
200
  }
201
201
  .fx-presence-chip-name {
202
202
  font-weight: 500;
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ProductVersion — `filex 0.43.0`, as a quiet line (lib/productVersion).
4
+ *
5
+ * The one piece every place that shows the version draws: the admin panel's
6
+ * account menu, the explorer's avatar menu, the user settings dialog. Written
7
+ * once here so the desktop shell (which draws the web app) and an embedding
8
+ * host (which draws its own chrome and can mount this) print the same line.
9
+ *
10
+ * ⚠ `<bdi>`: a Latin name and a dotted number inside a right-to-left menu
11
+ * must keep their own order, and the line still starts on the menu's start
12
+ * side. `dir="ltr"` on the element would flip its alignment as well.
13
+ *
14
+ * ⚠ Its rules are `.fe-version` in styles/base.css, not a `<style>` block
15
+ * here: a component's own styles reach only a bundle that imports it, and
16
+ * the two shipped stylesheets drift (lesson #416).
17
+ */
18
+ import { computed } from 'vue';
19
+ import { productVersionLine } from '../lib/productVersion';
20
+
21
+ const props = defineProps<{
22
+ /** The server's version string (capabilities). Unknown → nothing drawn. */
23
+ version?: string | null;
24
+ }>();
25
+
26
+ const line = computed(() => productVersionLine(props.version));
27
+ </script>
28
+
29
+ <template>
30
+ <p v-if="line" class="fe-version" data-testid="product-version"><bdi>{{ line }}</bdi></p>
31
+ </template>
@@ -26,6 +26,7 @@ import { computed, onBeforeUnmount, watch } from 'vue';
26
26
  import type { FileNode } from '../types/FileNode';
27
27
  import type { LocaleCode } from '../types/ExplorerConfig';
28
28
  import { useLocale } from '../composables/useLocale';
29
+ import { inlineKeyStep } from '../lib/direction';
29
30
  import { eventMatchesShortcut, shortcutHint } from '../composables/useKeyboardShortcuts';
30
31
  import PreviewModal from '../modals/PreviewModal.vue';
31
32
 
@@ -70,7 +71,9 @@ const emit = defineEmits<{
70
71
  (e: 'starred', value: boolean): void;
71
72
  }>();
72
73
 
73
- const { t } = useLocale(() => props.locale);
74
+ // ⚠ RTL: `dir` on the teleported hint, and ← is "next" in a right-to-left
75
+ // interface (the next file's chevron is on the left there).
76
+ const { t, dir } = useLocale(() => props.locale);
74
77
 
75
78
  function inFormControl(target: EventTarget | null): boolean {
76
79
  const el = target as HTMLElement | null;
@@ -114,19 +117,11 @@ function onKeydown(e: KeyboardEvent) {
114
117
  }
115
118
 
116
119
  if (e.ctrlKey || e.metaKey || e.altKey) return;
117
- switch (e.key) {
118
- case 'ArrowRight':
119
- case 'ArrowDown':
120
- e.preventDefault();
121
- e.stopPropagation();
122
- emit('nav', 1);
123
- break;
124
- case 'ArrowLeft':
125
- case 'ArrowUp':
126
- e.preventDefault();
127
- e.stopPropagation();
128
- emit('nav', -1);
129
- break;
120
+ const step = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : inlineKeyStep(e.key, dir.value);
121
+ if (step) {
122
+ e.preventDefault();
123
+ e.stopPropagation();
124
+ emit('nav', step);
130
125
  }
131
126
  }
132
127
 
@@ -200,6 +195,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown, true));
200
195
  <div
201
196
  v-if="open"
202
197
  class="fe fe-ql-hint"
198
+ :dir="dir"
203
199
  :class="{
204
200
  'fe--theme-light': theme === 'light',
205
201
  'fe--theme-dark': theme === 'dark',