@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
@@ -30,6 +30,8 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
30
30
  import type { ApiToken } from '../types/Tokens';
31
31
  import { useLocale } from '../composables/useLocale';
32
32
  import { useTokens } from '../composables/useTokens';
33
+ import DataTable, { type DataColumn } from './DataTable.vue';
34
+ import type { ContextAction } from './ContextMenu.vue';
33
35
  import { resolveLocale } from '../locales/resolve';
34
36
 
35
37
  const props = defineProps<{
@@ -89,6 +91,14 @@ const scopeState = ref<Record<string, boolean>>({
89
91
  const rootPath = ref('');
90
92
  const expiresInDays = ref<number | null>(null);
91
93
 
94
+ /**
95
+ * ⚠⚠ Nothing ticked is not a request (owner's decision, v0.43.0): no door
96
+ * mints a token without an explicit list, and this form used to paper over
97
+ * an empty one with `|| 'read'` — a silent default, the very thing the rule
98
+ * removes. The button waits for a tick and says why.
99
+ */
100
+ const noScope = computed(() => !FULL_SCOPES.some((s) => scopeState.value[s]));
101
+
92
102
  function buildScopes(): string {
93
103
  const parts = FULL_SCOPES.filter((s) => scopeState.value[s]) as string[];
94
104
  const root = rootPath.value.trim();
@@ -131,9 +141,8 @@ async function mint(): Promise<void> {
131
141
  copied.value = false;
132
142
  try {
133
143
  if (props.full) {
134
- // At least one verb, or the token can do nothing and the server's
135
- // refusal would be about the wrong thing.
136
- const scopes = buildScopes() || 'read';
144
+ if (noScope.value) return;
145
+ const scopes = buildScopes();
137
146
  await create({
138
147
  label: label.value.trim() || defaultLabel(),
139
148
  scopes,
@@ -160,6 +169,50 @@ async function mint(): Promise<void> {
160
169
  }
161
170
  }
162
171
 
172
+ /** The list's columns — the product's one table (DataTable). */
173
+ const columns = computed<DataColumn<ApiToken>[]>(() => [
174
+ {
175
+ id: 'label',
176
+ label: t('conn.tokens.col.label'),
177
+ sortable: true,
178
+ width: 200,
179
+ format: (row) => row.label || '—',
180
+ },
181
+ { id: 'scopes', label: t('conn.tokens.col.scopes'), sortable: true, width: 200 },
182
+ {
183
+ id: 'used',
184
+ label: t('conn.tokens.col.used'),
185
+ sortable: true,
186
+ sortDir: 'desc',
187
+ width: 140,
188
+ format: usedLabel,
189
+ sortValue: (row) => (row.last_used_at ? new Date(row.last_used_at).getTime() : null),
190
+ },
191
+ ]);
192
+
193
+ /** The row's one verb, behind its one `Actions` control. ⚠ It keeps the
194
+ * two-step confirmation it had as a loose button: the first pick arms it and
195
+ * the label becomes "Confirm", the second pick revokes the token — which
196
+ * also ends any session already open on it. */
197
+ function rowActions(row: ApiToken): ContextAction[] {
198
+ return [
199
+ {
200
+ key: 'revoke',
201
+ label: confirming.value === row.id ? t('conn.tokens.confirm') : t('conn.tokens.revoke'),
202
+ icon: 'delete',
203
+ danger: true,
204
+ /* The whole control used to be `:disabled="busy"`; the table draws the
205
+ control, so the verb carries it — RowActions greys a button with
206
+ nothing usable behind it. */
207
+ disabled: busy.value,
208
+ },
209
+ ];
210
+ }
211
+
212
+ function onRowAction(key: string, row: ApiToken) {
213
+ if (key === 'revoke') void revoke(row);
214
+ }
215
+
163
216
  async function revoke(row: ApiToken): Promise<void> {
164
217
  if (confirming.value !== row.id) {
165
218
  confirming.value = row.id;
@@ -239,6 +292,9 @@ function usedLabel(row: ApiToken): string {
239
292
  <input v-model="scopeState[s]" type="checkbox" :data-testid="`token-scope-${s}`" />
240
293
  <span>{{ s }}</span>
241
294
  </label>
295
+ <p v-if="noScope" class="fe-s3keys__hint fe-tokform__required" role="alert" data-testid="token-scopes-required">
296
+ {{ t('conn.tokens.scopesRequired') }}
297
+ </p>
242
298
  </fieldset>
243
299
 
244
300
  <label class="fe-tokform__field">
@@ -263,7 +319,7 @@ function usedLabel(row: ApiToken): string {
263
319
  data-testid="token-expiry"
264
320
  />
265
321
  </label>
266
- <button class="fe-s3keys__btn" :disabled="busy" data-testid="token-mint" @click="mint">
322
+ <button class="fe-s3keys__btn" :disabled="busy || noScope" data-testid="token-mint" @click="mint">
267
323
  {{ t('conn.tokens.mint') }}
268
324
  </button>
269
325
  </div>
@@ -287,30 +343,29 @@ function usedLabel(row: ApiToken): string {
287
343
  </button>
288
344
  </div>
289
345
 
346
+ <!-- ⚠ THE table (DataTable — the explorer's own), not a table of its own.
347
+ This panel is reachable from the admin panel's Connections menu and
348
+ from the explorer, so its list resizes, sorts, hides and moves columns
349
+ and remembers that on the account (`conn.tokens`), like every other
350
+ table. The raised ground is the panel's own (`.fe-s3keys` sets
351
+ `--tbl-bg` once). -->
290
352
  <p v-if="loading" class="fe-s3keys__muted">…</p>
291
- <table v-else-if="tokens.length" class="fe-s3keys__table">
292
- <thead>
293
- <tr>
294
- <th>{{ t('conn.tokens.col.label') }}</th>
295
- <th>{{ t('conn.tokens.col.scopes') }}</th>
296
- <th>{{ t('conn.tokens.col.used') }}</th>
297
- <th></th>
298
- </tr>
299
- </thead>
300
- <tbody>
301
- <tr v-for="row in tokens" :key="row.id">
302
- <td>{{ row.label || '—' }}</td>
303
- <td><code>{{ row.scopes }}</code></td>
304
- <td>{{ usedLabel(row) }}</td>
305
- <td class="fe-s3keys__actions">
306
- <button class="fe-s3keys__link is-danger" :disabled="busy" @click="revoke(row)">
307
- {{ confirming === row.id ? t('conn.tokens.confirm') : t('conn.tokens.revoke') }}
308
- </button>
309
- </td>
310
- </tr>
311
- </tbody>
312
- </table>
313
- <p v-else-if="canMint" class="fe-s3keys__muted">{{ t('conn.tokens.empty') }}</p>
353
+ <DataTable
354
+ v-else
355
+ table-id="conn.tokens"
356
+ :columns="columns"
357
+ :rows="tokens"
358
+ row-key="id"
359
+ :locale="locale"
360
+ :empty="t('conn.tokens.empty')"
361
+ :row-actions="rowActions"
362
+ :row-actions-test-id="(row: ApiToken) => `api-token-actions-${row.id}`"
363
+ @row-action="(key: string, row: ApiToken) => onRowAction(key, row)"
364
+ >
365
+ <template #cell-scopes="{ row }">
366
+ <code class="tbl-mono">{{ row.scopes }}</code>
367
+ </template>
368
+ </DataTable>
314
369
 
315
370
  <!-- ⚠ Said next to the button rather than in a document nobody opens: a
316
371
  revoked token stops a session that is already open, not only the next
@@ -20,6 +20,7 @@ import ContextMenu, { type ContextAction } from './ContextMenu.vue';
20
20
  * `.fe-subhead__actions`), so the header and the crumb row can no longer end
21
21
  * up offering different view modes — which is what two mount points cost. */
22
22
  import { useLocale } from '../composables/useLocale';
23
+ import { inlineEndX } from '../lib/direction';
23
24
  import { actionIconSvg } from '../lib/actionIcons';
24
25
  import { eventMatchesShortcut, menuShortcutHint, shortcutHint } from '../composables/useKeyboardShortcuts';
25
26
 
@@ -253,7 +254,7 @@ function toggleDensity() {
253
254
  emit('update:density', density.value);
254
255
  }
255
256
 
256
- const { t } = useLocale(() => props.locale);
257
+ const { t, dir } = useLocale(() => props.locale);
257
258
 
258
259
  const searchEl = ref<HTMLInputElement | null>(null);
259
260
  const localSearch = ref(props.searchQuery);
@@ -333,11 +334,30 @@ const hasSelection = computed(() => mode.value !== 'none');
333
334
  const barSplit = computed(() => {
334
335
  const icons: ContextAction[] = [];
335
336
  const rest: ContextAction[] = [];
336
- for (const a of toolbarItems.value) {
337
+ /* ⚠ Walks the parent's list WITH its dividers, not `toolbarItems`. The "⋯"
338
+ * is a dropdown drawn by the same ContextMenu as the right click, and the
339
+ * dividers are what group it: the owner asked for a line between every
340
+ * app's actions (lib/pluginMenu → one `sep-plugin:*` per app), and a "⋯"
341
+ * built from the divider-free list ran Open, Rename, Tags and two apps'
342
+ * verbs together as one undivided column while the right click beside it
343
+ * drew the lines. An icon never takes a divider; ContextMenu collapses the
344
+ * ones that end up leading, trailing or doubled once the icons are pulled
345
+ * out. `restCount` is the number of REAL entries — a "⋯" whose menu is
346
+ * nothing but dividers would open an empty box. */
347
+ let restCount = 0;
348
+ for (const a of props.actions) {
349
+ if (a.hidden) continue;
350
+ if (a.divider) {
351
+ rest.push(a);
352
+ continue;
353
+ }
337
354
  if (BAR_ICON_KEYS.has(a.key) && iconFor(a)) icons.push(a);
338
- else rest.push(a);
355
+ else {
356
+ rest.push(a);
357
+ restCount++;
358
+ }
339
359
  }
340
- return { icons, rest };
360
+ return { icons, rest: restCount ? rest : [], restCount };
341
361
  });
342
362
 
343
363
  /* ── folding (kept from ui-fix, moved onto the bar) ───────────────────
@@ -412,7 +432,9 @@ watch(
412
432
 
413
433
  function openBarMore() {
414
434
  const r = selMoreBtnEl.value?.getBoundingClientRect();
415
- selMoreRef.value?.show({ clientX: r ? r.right : 0, clientY: r ? r.bottom + 4 : 0 } as MouseEvent, []);
435
+ // ⚠ RTL: from the button's END edge (its left in RTL) — ContextMenu flips it
436
+ // back under the button from there, in either direction.
437
+ selMoreRef.value?.show({ clientX: r ? inlineEndX(r, dir.value) : 0, clientY: r ? r.bottom + 4 : 0 } as MouseEvent, []);
416
438
  }
417
439
 
418
440
  /* ── where the bar lands ──────────────────────────────────────────────── */
@@ -894,7 +916,7 @@ const menuActions = computed<ContextAction[]>(() => {
894
916
 
895
917
  function openMore() {
896
918
  const r = moreBtnEl.value?.getBoundingClientRect();
897
- moreRef.value?.show({ clientX: r ? r.right : 0, clientY: r ? r.bottom + 4 : 0 }, []);
919
+ moreRef.value?.show({ clientX: r ? inlineEndX(r, dir.value) : 0, clientY: r ? r.bottom + 4 : 0 }, []); // ⚠ RTL: END edge, see openBarMore
898
920
  }
899
921
 
900
922
  function onMoreSelect(a: ContextAction) {
@@ -0,0 +1,54 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * PluginConfirmModal — the question an action's manifest `confirm` text asks
4
+ * before the job is queued. The same small dialog every surface uses
5
+ * (`modals/Modal`), so a plugin's confirmation looks like the trash's.
6
+ */
7
+ import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
8
+ import { useLocale } from '../../composables/useLocale';
9
+ import Modal from '../../modals/Modal.vue';
10
+
11
+ const props = defineProps<{
12
+ open: boolean;
13
+ locale: LocaleCode;
14
+ theme?: ThemeMode;
15
+ /** The action's label — the dialog's title. */
16
+ title: string;
17
+ /** The manifest's `confirm` text in the viewer's language. */
18
+ message: string;
19
+ danger?: boolean;
20
+ }>();
21
+
22
+ const emit = defineEmits<{
23
+ (e: 'close'): void;
24
+ (e: 'confirm'): void;
25
+ }>();
26
+
27
+ const { t } = useLocale(() => props.locale);
28
+ </script>
29
+
30
+ <template>
31
+ <Modal
32
+ :open="open"
33
+ :title="title || t('plugin.confirm.title')"
34
+ size="sm"
35
+ :theme="theme"
36
+ @close="emit('close')"
37
+ >
38
+ <p data-testid="plugin-confirm-message">{{ message }}</p>
39
+ <template #actions>
40
+ <button type="button" class="fe-btn" @click="emit('close')">
41
+ {{ t('plugin.confirm.cancel') }}
42
+ </button>
43
+ <button
44
+ type="button"
45
+ class="fe-btn"
46
+ :class="danger ? 'fe-btn--danger' : 'fe-btn--primary'"
47
+ data-testid="plugin-confirm-run"
48
+ @click="emit('confirm')"
49
+ >
50
+ {{ t('plugin.confirm.run') }}
51
+ </button>
52
+ </template>
53
+ </Modal>
54
+ </template>
@@ -0,0 +1,149 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * PluginInspectorSection — one `inspector` view of an app plugin, as a
4
+ * collapsible section of the details panel.
5
+ *
6
+ * Closed, it is a heading and nothing else: no request is made until the
7
+ * person opens it (a panel that loaded every plugin's view for every click
8
+ * would make N calls per selection). Opened, it asks
9
+ * `GET …/views/{plugin}/{view}?path=` once for the selected item and draws
10
+ * the surface inline; the footer buttons post events through the same
11
+ * conversation the modal holds (`usePluginSurface`), and a `{op}` answer
12
+ * goes up for the host to register in the tray, exactly as M1's modal does.
13
+ * A new selection resets the section to closed-and-unloaded.
14
+ */
15
+ import { computed, ref, watch } from 'vue';
16
+ import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
17
+ import type { FileApi } from '../../composables/useFileApi';
18
+ import type { PluginViewRow, SurfaceOpenRequest } from '../../types/Plugins';
19
+ import { useLocale } from '../../composables/useLocale';
20
+ import { usePluginSurface } from '../../composables/usePluginSurface';
21
+ import { labelOf } from '../../lib/pluginLabel';
22
+ import SurfaceConversation from './SurfaceConversation.vue';
23
+ import SurfaceFooterButtons from './SurfaceFooterButtons.vue';
24
+
25
+ const props = defineProps<{
26
+ api: FileApi;
27
+ view: PluginViewRow;
28
+ /** Adapter-qualified path of the selected item. */
29
+ path: string;
30
+ locale: LocaleCode;
31
+ theme?: ThemeMode;
32
+ storages?: string[];
33
+ }>();
34
+
35
+ const emit = defineEmits<{
36
+ (e: 'op', op: Record<string, unknown>): void;
37
+ (e: 'toast', message: string): void;
38
+ /** v3 §3.0 — the answer says "go to this file"; the explorer navigates. */
39
+ (e: 'open', req: SurfaceOpenRequest): void;
40
+ }>();
41
+
42
+ const { t } = useLocale(() => props.locale);
43
+
44
+ const expanded = ref(false);
45
+ const loading = ref(false);
46
+ const loadError = ref('');
47
+
48
+ const conv = usePluginSurface(
49
+ {
50
+ api: props.api,
51
+ plugin: props.view.plugin,
52
+ view: props.view.id,
53
+ path: () => props.path,
54
+ locale: () => props.locale,
55
+ errorText: () => t('plugin.view.error'),
56
+ },
57
+ {
58
+ onOp: (op) => emit('op', op),
59
+ onOpen: (req) => emit('open', req),
60
+ // The conversation ended (a job was queued, or the plugin said `done`):
61
+ // what the section shows next is the view's fresh state.
62
+ onDone: () => void load(),
63
+ onToast: (m) => emit('toast', m),
64
+ },
65
+ );
66
+
67
+ const label = computed(() => labelOf(props.view.label, props.locale) || props.view.id);
68
+ const sectionId = computed(() => `fe-plugin-insp-${props.view.plugin}-${props.view.id}`);
69
+
70
+ async function load() {
71
+ loading.value = true;
72
+ loadError.value = '';
73
+ try {
74
+ const res = await props.api.pluginView(props.view.plugin, props.view.id, props.path);
75
+ conv.setSurface(res?.surface ?? null);
76
+ if (!res?.surface) loadError.value = t('plugin.view.error');
77
+ } catch (e) {
78
+ conv.setSurface(null);
79
+ // ⚠ An app's own error text never met the catalogue: isolate its
80
+ // machine runs for the reader's direction (lib/direction).
81
+ loadError.value = t.foreign?.(String((e as Error)?.message ?? e)) || t('plugin.view.error');
82
+ } finally {
83
+ loading.value = false;
84
+ }
85
+ }
86
+
87
+ function toggle() {
88
+ expanded.value = !expanded.value;
89
+ if (expanded.value && !conv.current.value && !loading.value) void load();
90
+ }
91
+
92
+ watch(
93
+ () => props.path,
94
+ () => {
95
+ expanded.value = false;
96
+ loadError.value = '';
97
+ conv.setSurface(null);
98
+ },
99
+ );
100
+ </script>
101
+
102
+ <template>
103
+ <section class="fe-inspector__section fe-pinsp" :data-testid="`inspector-plugin-${view.plugin}-${view.id}`">
104
+ <h3 class="fe-inspector__heading fe-pinsp__heading">
105
+ <button
106
+ type="button"
107
+ class="fe-pinsp__toggle"
108
+ :aria-expanded="expanded"
109
+ :aria-controls="sectionId"
110
+ :data-testid="`inspector-plugin-toggle-${view.plugin}-${view.id}`"
111
+ @click="toggle"
112
+ >
113
+ <svg
114
+ class="fe-ficon fe-pinsp__chev"
115
+ :class="{ 'is-open': expanded }"
116
+ viewBox="0 0 24 24"
117
+ fill="none"
118
+ stroke="currentColor"
119
+ stroke-width="2"
120
+ stroke-linecap="round"
121
+ stroke-linejoin="round"
122
+ aria-hidden="true"
123
+ focusable="false"
124
+ >
125
+ <path d="M9 5l7 7-7 7" />
126
+ </svg>
127
+ <span>{{ label }}</span>
128
+ </button>
129
+ </h3>
130
+ <div v-if="expanded" :id="sectionId" class="fe-pinsp__body">
131
+ <p v-if="loading" class="fe-inspector__empty">{{ t('plugin.view.loading') }}</p>
132
+ <p v-else-if="loadError" class="fe-surface__error" role="alert">{{ loadError }}</p>
133
+ <template v-else-if="conv.current.value">
134
+ <SurfaceConversation
135
+ :conv="conv"
136
+ :locale="locale"
137
+ :theme="theme"
138
+ :api="api"
139
+ :plugin="view.plugin"
140
+ :storages="storages"
141
+ :start-at="path"
142
+ />
143
+ <div v-if="conv.footer.value.length" class="fe-pinsp__actions">
144
+ <SurfaceFooterButtons :conv="conv" :locale="locale" testid-prefix="inspector-plugin" small />
145
+ </div>
146
+ </template>
147
+ </div>
148
+ </section>
149
+ </template>