@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
@@ -190,6 +190,8 @@ const props = withDefaults(
190
190
  folderKey?: string;
191
191
  /** Print each row's parent folder (a search, a cross-folder view). */
192
192
  showParentPath?: boolean;
193
+ /** These rows are the Trash (ListView `trash`: deleted-on, deleted-from, time left). */
194
+ trash?: boolean;
193
195
  /** Paths that were cut — dimmed, and never dragged. */
194
196
  clipped?: Set<string>;
195
197
  /** A second narrowing composed AFTER this pane's own filter row (the
@@ -910,6 +912,7 @@ watch(panePath, () => {
910
912
  :selected="selected"
911
913
  :clipped="clipped"
912
914
  :show-parent-path="showParentPath"
915
+ :trash="trash"
913
916
  :locale="locale"
914
917
  :loading="paneLoading"
915
918
  :folder-key="folderKey"
@@ -68,6 +68,7 @@
68
68
  */
69
69
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
70
70
  import { useLocale } from '../composables/useLocale';
71
+ import { clampAlongInline, inlineStartX } from '../lib/direction';
71
72
  import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
72
73
  import type { FileNode } from '../types/FileNode';
73
74
  import type { DriveFilters, ModifiedFilter, SizeFilter, TypeFilter } from '../lib/fileFilters';
@@ -153,7 +154,9 @@ const emit = defineEmits<{
153
154
  (e: 'select-all'): void;
154
155
  }>();
155
156
 
156
- const { t } = useLocale(() => props.locale);
157
+ // ⚠ RTL: `dir` goes on the teleported popover (under <body> it would take the
158
+ // host page's direction) and steers where it hangs from its chip.
159
+ const { t, dir } = useLocale(() => props.locale);
157
160
 
158
161
  /** surucu:d1-scope — everything except the name box is drawn only in `full`. */
159
162
  const full = computed(() => (props.mode ?? 'full') === 'full');
@@ -356,7 +359,9 @@ async function toggle(id: PopId) {
356
359
  return;
357
360
  }
358
361
  const r = anchorEls.value[id]?.getBoundingClientRect();
359
- pos.value = { x: r ? r.left : 8, y: r ? r.bottom + 6 : 8 };
362
+ // ⚠ RTL: hung from the chip's START edge — its right side in RTL.
363
+ const anchor = r ? inlineStartX(r, dir.value) : 8;
364
+ pos.value = { x: anchor, y: r ? r.bottom + 6 : 8 };
360
365
  openPop.value = id;
361
366
  await nextTick();
362
367
  // Keep it on screen: a chip near the right edge would otherwise open a panel
@@ -364,9 +369,10 @@ async function toggle(id: PopId) {
364
369
  const panel = panelEl.value;
365
370
  if (panel) {
366
371
  const box = panel.getBoundingClientRect();
367
- if (box.right > window.innerWidth - 8) {
368
- pos.value = { ...pos.value, x: Math.max(8, window.innerWidth - 8 - box.width) };
369
- }
372
+ /* ⚠ RTL: it runs toward the inline END of the chip (leftward in RTL) and is
373
+ pushed back from that edge — lib/direction; LTR is the arithmetic this
374
+ block always did. */
375
+ pos.value = { ...pos.value, x: clampAlongInline(anchor, box.width, window.innerWidth, dir.value) };
370
376
  if (box.bottom > window.innerHeight - 8) {
371
377
  const r2 = anchorEls.value[id]?.getBoundingClientRect();
372
378
  pos.value = { ...pos.value, y: Math.max(8, (r2 ? r2.top : 0) - box.height - 6) };
@@ -675,6 +681,7 @@ onBeforeUnmount(() => {
675
681
  v-if="openPop"
676
682
  ref="panelEl"
677
683
  class="fe-filterpop"
684
+ :dir="dir"
678
685
  :class="{
679
686
  'fe--theme-light': theme === 'light',
680
687
  'fe--theme-dark': theme === 'dark',
@@ -13,6 +13,8 @@ import { hasInternalDrag } from '../lib/dragOut';
13
13
  import type { FileNode } from '../types/FileNode';
14
14
  import type { LocaleCode } from '../types/ExplorerConfig';
15
15
  import { useLocale } from '../composables/useLocale';
16
+ import { lockOf, lockWords } from '../lib/appLock';
17
+ import { linkWordsFor } from '../lib/symlink'; /* issue #34 — a link that will not open */
16
18
  import { checkMod, clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
17
19
  import ItemCheck from './ItemCheck.vue';
18
20
  import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
@@ -30,6 +32,7 @@ import {
30
32
  groupingActive,
31
33
  } from '../lib/dateGroups'; /* gruplama */
32
34
  import { useSortStore, type ListingOrder } from '../lib/sortOrder'; /* gruplama */
35
+ import { contentDir } from '../lib/direction';
33
36
 
34
37
  const props = withDefaults(defineProps<{
35
38
  files: FileNode[];
@@ -98,6 +101,25 @@ const emit = defineEmits<{
98
101
  const { t, formatSize, nodeDisplayName, formatDate, formatMonthYear, zonedYearMonth, toDate } =
99
102
  useLocale(() => props.locale);
100
103
 
104
+ /* App plugins — an app's hold on a row (docs/APP-PLUGINS-API.md → "File
105
+ locks"). The listing already caps `perm` at viewer, so the write verbs are
106
+ gone without this; the badge is what stops that reading as a bug. ⚠ One
107
+ source of words for every view and the details panel (lib/appLock). */
108
+ function lockTitleOf(n: FileNode): string {
109
+ return lockWords(lockOf(n), { t, formatDate, locale: props.locale });
110
+ }
111
+
112
+ /* issue #34 — a symlink the server will not follow. Same shape as the lock
113
+ badge above and for the same reason: the words live in ONE module
114
+ (lib/symlink), so the row, the details panel and the toast that explains a
115
+ refused open cannot drift apart. `null` for every ordinary row — a link
116
+ whose target is inside the root was already followed and arrives as that
117
+ target, so it is not one of these. */
118
+ function linkOf(n: FileNode) {
119
+ return linkWordsFor(n, { t });
120
+ }
121
+
122
+
101
123
  /* gruplama — this pane's sort, by injection, so the gallery cannot disagree
102
124
  * with the list beside it about whether a date heading is honest right now. */
103
125
  const sort = useSortStore();
@@ -305,6 +327,7 @@ function metaFor(n: FileNode): string {
305
327
  v-for="p in previewFor(n)"
306
328
  :key="'fprev'"
307
329
  class="fe-fprev"
330
+ :dir="contentDir(p.kind)"
308
331
  :class="'fe-fprev--' + p.kind"
309
332
  :style="p.kind === 'table' ? { '--fprev-cols': String(p.cols) } : undefined"
310
333
  aria-hidden="true"
@@ -401,11 +424,36 @@ function metaFor(n: FileNode): string {
401
424
  v-if="showParentPath && parentDirOf(n.path)"
402
425
  class="fe-gal__meta-line fe-gal__meta-line--path"
403
426
  :title="parentDirOf(n.path)"
404
- >{{ parentDirOf(n.path) }}</span>
427
+ ><bdi>{{ parentDirOf(n.path) }}</bdi></span>
405
428
  </div>
406
429
  </div>
407
430
  <div class="fe-gal__label" :title="n.basename">
408
- {{ nodeDisplayName(n) }}
431
+ <bdi>{{ nodeDisplayName(n) }}</bdi>
432
+ </div>
433
+ <!-- issue #34 — a symlink the server will not follow. Its own line: a
434
+ gallery card has no caption row to hang it off, and a badge that
435
+ only appeared in two of the three views is how the same fix reaches
436
+ some people and not others. -->
437
+ <div v-if="linkOf(n)" class="fe-gal__meta-line">
438
+ <span
439
+ class="fe-symlink"
440
+ :class="'fe-symlink--' + linkOf(n)!.state"
441
+ role="img"
442
+ :title="linkOf(n)!.why"
443
+ :aria-label="linkOf(n)!.why"
444
+ data-testid="symlink-badge"
445
+ :data-link-state="linkOf(n)!.state"
446
+ ><span class="fe-symlink__glyph" aria-hidden="true">&#128279;</span>{{ linkOf(n)!.badge }}</span>
447
+ </div>
448
+ <div v-if="lockTitleOf(n)" class="fe-gal__meta-line">
449
+ <span
450
+ v-if="lockTitleOf(n)"
451
+ class="fe-applock"
452
+ role="img"
453
+ :title="lockTitleOf(n)"
454
+ :aria-label="lockTitleOf(n)"
455
+ data-testid="lock-badge"
456
+ ><span class="fe-applock__glyph" aria-hidden="true">&#128274;</span>{{ t('applock.badge') }}</span>
409
457
  </div>
410
458
  </div>
411
459
  </template>
@@ -20,6 +20,8 @@ import type { FileNode } from '../types/FileNode';
20
20
  import { hasInternalDrag } from '../lib/dragOut';
21
21
  import type { LocaleCode } from '../types/ExplorerConfig';
22
22
  import { useLocale } from '../composables/useLocale';
23
+ import { lockOf, lockWords } from '../lib/appLock';
24
+ import { linkWordsFor } from '../lib/symlink'; /* issue #34 — a link that will not open */
23
25
  import { checkMod, clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
24
26
  import ItemCheck from './ItemCheck.vue';
25
27
  import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
@@ -38,6 +40,7 @@ import { useSortStore, type ListingOrder } from '../lib/sortOrder'; /* gruplama
38
40
  import StarButton from './StarButton.vue';
39
41
  import { snippetSegments } from '../lib/snippet'; /* bul:s3 */
40
42
  import { applyDragGhost } from '../lib/dragGhost'; /* wiring:c4 */
43
+ import { contentDir } from '../lib/direction';
41
44
 
42
45
  const props = withDefaults(defineProps<{
43
46
  files: FileNode[];
@@ -152,6 +155,25 @@ const emit = defineEmits<{
152
155
  const { t, formatSize, nodeDisplayName, formatDate, formatMonthYear, zonedYearMonth, toDate } =
153
156
  useLocale(() => props.locale);
154
157
 
158
+ /* App plugins — an app's hold on a row (docs/APP-PLUGINS-API.md → "File
159
+ locks"). The listing already caps `perm` at viewer, so the write verbs are
160
+ gone without this; the badge is what stops that reading as a bug. ⚠ One
161
+ source of words for every view and the details panel (lib/appLock). */
162
+ function lockTitleOf(n: FileNode): string {
163
+ return lockWords(lockOf(n), { t, formatDate, locale: props.locale });
164
+ }
165
+
166
+ /* issue #34 — a symlink the server will not follow. Same shape as the lock
167
+ badge above and for the same reason: the words live in ONE module
168
+ (lib/symlink), so the row, the details panel and the toast that explains a
169
+ refused open cannot drift apart. `null` for every ordinary row — a link
170
+ whose target is inside the root was already followed and arrives as that
171
+ target, so it is not one of these. */
172
+ function linkOf(n: FileNode) {
173
+ return linkWordsFor(n, { t });
174
+ }
175
+
176
+
155
177
  const ordered = computed<FileNode[]>(() => [...props.files].sort(byFoldersFirst));
156
178
  watch(
157
179
  ordered,
@@ -459,6 +481,7 @@ function snippetTitle(snippet: string): string {
459
481
  v-for="p in previewFor(n)"
460
482
  :key="'fprev'"
461
483
  class="fe-fprev"
484
+ :dir="contentDir(p.kind)"
462
485
  :class="'fe-fprev--' + p.kind"
463
486
  :style="p.kind === 'table' ? { '--fprev-cols': String(p.cols) } : undefined"
464
487
  aria-hidden="true"
@@ -576,7 +599,7 @@ function snippetTitle(snippet: string): string {
576
599
  </span>
577
600
  <div class="fe-grid__main">
578
601
  <div class="fe-grid__label" :title="n.basename">
579
- {{ nodeDisplayName(n) }}
602
+ <bdi>{{ nodeDisplayName(n) }}</bdi>
580
603
  <span
581
604
  v-if="keepBadgeFor && keepBadgeFor(n)"
582
605
  :class="['fe-keepbadge', 'fe-keepbadge--' + keepBadgeFor(n)]"
@@ -585,7 +608,32 @@ function snippetTitle(snippet: string): string {
585
608
  :aria-label="t('keep.badge_' + keepBadgeFor(n))"
586
609
  >{{ keepGlyph(keepBadgeFor(n)!) }}</span>
587
610
  </div>
588
- <div class="fe-grid__meta">{{ captionFor(n) }}</div>
611
+ <!-- The whole caption on hover: the card has room for "110 B • Sep 22,
612
+ 2026" and not for every language's date (German "22. Sept. 2026"
613
+ lost its year to the ellipsis, v0.43.0 translator pass). -->
614
+ <div class="fe-grid__meta" :title="captionFor(n)">
615
+ <span
616
+ v-if="lockTitleOf(n)"
617
+ class="fe-applock"
618
+ role="img"
619
+ :title="lockTitleOf(n)"
620
+ :aria-label="lockTitleOf(n)"
621
+ data-testid="lock-badge"
622
+ ><span class="fe-applock__glyph" aria-hidden="true">&#128274;</span>{{ t('applock.badge') }}</span>
623
+ <!-- issue #34 — a symlink the server will not follow. Same badge,
624
+ same words, same module as the list and the gallery. -->
625
+ <span
626
+ v-if="linkOf(n)"
627
+ class="fe-symlink"
628
+ :class="'fe-symlink--' + linkOf(n)!.state"
629
+ role="img"
630
+ :title="linkOf(n)!.why"
631
+ :aria-label="linkOf(n)!.why"
632
+ data-testid="symlink-badge"
633
+ :data-link-state="linkOf(n)!.state"
634
+ ><span class="fe-symlink__glyph" aria-hidden="true">&#128279;</span>{{ linkOf(n)!.badge }}</span>
635
+ {{ captionFor(n) }}
636
+ </div>
589
637
  <!-- A card at a storage's root has no parent folder to name: no line,
590
638
  not an em dash — the same rule the gallery follows and the list's
591
639
  Location cell (tablo:t1). The dash read as a stray character under
@@ -594,7 +642,7 @@ function snippetTitle(snippet: string): string {
594
642
  v-if="showParentPath && parentDirOf(n.path)"
595
643
  class="fe-grid__parent"
596
644
  :title="parentDirOf(n.path)"
597
- >{{ parentDirOf(n.path) }}</div>
645
+ ><bdi>{{ parentDirOf(n.path) }}</bdi></div>
598
646
  <!-- bul:s3 — content snippet («» → <mark> via TEXT segments, no innerHTML) -->
599
647
  <div v-if="cardSnippet(n)" class="fe-grid__snippet" :title="snippetTitle(cardSnippet(n))">
600
648
  <template v-for="(seg, si) in snippetSegments(cardSnippet(n))" :key="si">
@@ -30,6 +30,7 @@
30
30
  * hash does not match in the web-component build, so the rules would silently
31
31
  * stop applying in every embed.
32
32
  */
33
+ import StorageTags from './StorageTags.vue';
33
34
  import { computed, ref } from 'vue';
34
35
  import type { FileNode } from '../types/FileNode';
35
36
  import type { LocaleCode } from '../types/ExplorerConfig';
@@ -144,12 +145,9 @@ const storageTile = fileIconTile({ type: 'dir', mime_type: 'inode/storage' });
144
145
  * with the wrong size line says something false.
145
146
  */
146
147
  function storageCaption(s: HomeStorage): string {
147
- const base =
148
- typeof s.usedBytes === 'number' && s.usedBytes >= 0
149
- ? t('drive.storage.used_unlimited', { used: formatSize(s.usedBytes) })
150
- : t('conn.guide.storage');
151
- // A read-only mount says so on its card too (issue #30).
152
- return s.readOnly ? `${base} · ${t('sidenav.storage.readOnly')}` : base;
148
+ return typeof s.usedBytes === 'number' && s.usedBytes >= 0
149
+ ? t('drive.storage.used_unlimited', { used: formatSize(s.usedBytes) })
150
+ : t('conn.guide.storage');
153
151
  }
154
152
 
155
153
  /**
@@ -202,6 +200,20 @@ function openNode(n: FileNode) {
202
200
  <span class="fe-home__storage-main">
203
201
  <span class="fe-home__storage-label">{{ s.label || s.name }}</span>
204
202
  <span class="fe-home__storage-meta">{{ storageCaption(s) }}</span>
203
+ <!-- A read-only mount says so on its card too (issue #30) — on a
204
+ line of its own, the panel's own mark (StorageTags, the one
205
+ read-only tag of every surface). ⚠ It used to be glued to
206
+ the size ("81 GB belegt · Nur lesen") inside a fixed 124 px
207
+ caption, and in German the words that mattered were the ones
208
+ the ellipsis ate (translator measurement, v0.43.0). A tag of
209
+ its own fits any language that fits the panel's. -->
210
+ <StorageTags
211
+ v-if="s.readOnly"
212
+ read-only
213
+ :locale="locale"
214
+ class="fe-home__storage-ro"
215
+ data-testid="home-storage-readonly"
216
+ />
205
217
  </span>
206
218
  </button>
207
219
  </div>
@@ -26,11 +26,18 @@
26
26
  import { computed, ref, watch } from 'vue';
27
27
  import type { FileApi, Grant, NodeVersion } from '../composables/useFileApi';
28
28
  import type { FileNode, ShareInfo } from '../types/FileNode';
29
- import type { LocaleCode } from '../types/ExplorerConfig';
30
- import { useLocale } from '../composables/useLocale';
31
- import { fileIconTile, typeLabelFor } from '../lib/fileIcons';
29
+ import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
30
+ import type { PluginViewRow } from '../types/Plugins';
31
+ import { localeTag, useLocale } from '../composables/useLocale';
32
+ import { fileIconTile, isStorageRow, typeLabelFor } from '../lib/fileIcons';
32
33
  import { actionIconSvg } from '../lib/actionIcons';
34
+ import { appliesToNodes } from '../lib/pluginApplies';
35
+ import { lockedRefusal, lockOf, lockWords } from '../lib/appLock';
36
+ import { linkWordsFor } from '../lib/symlink'; /* issue #34 */
37
+ import { personInitial as initialOfPerson, personName as nameOfPerson } from '../lib/personName';
33
38
  import TagPicker from './TagPicker.vue';
39
+ import type { TagKind } from '../lib/tags';
40
+ import PluginInspectorSection from './plugin/PluginInspectorSection.vue';
34
41
 
35
42
  const props = withDefaults(
36
43
  defineProps<{
@@ -59,6 +66,16 @@ const props = withDefaults(
59
66
  locale: LocaleCode;
60
67
  /** Narrow/embed mode → full-size overlay presentation. */
61
68
  narrow?: boolean;
69
+ /**
70
+ * Is the reader an administrator (`capabilities.caller_admin`)?
71
+ *
72
+ * ⚠ Only the NODE ID row depends on it (Burak, 2026-09-23). The id is a
73
+ * support handle — what an administrator quotes in Admin → File history or
74
+ * an audit row — and it means nothing to anybody else; on a shared surface
75
+ * it was a number in front of every reader. Absent = not an administrator,
76
+ * which is the safe answer for an embed that never asks the server.
77
+ */
78
+ callerAdmin?: boolean;
62
79
  /** Authenticated thumbnail resolver (useThumbs.src). Optional. */
63
80
  thumbSrc?: (n: FileNode) => string | null;
64
81
  /* === surucu:d1 — the Drive shell's details panel ===================== */
@@ -96,6 +113,17 @@ const props = withDefaults(
96
113
  apiBase?: string;
97
114
  authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
98
115
  authCredentials?: RequestCredentials;
116
+ /* === App plugins (docs/APP-PLUGINS-API.md → Placements, `inspector`) ===
117
+ * The `views[]` rows with placement `inspector`, as the host fetched them
118
+ * (`usePluginActions.views`); `[]` while the feature is off. One collapsible
119
+ * section per row whose `applies` rule accepts the selected SINGLE item —
120
+ * the client-side mirror the menu uses (lib/pluginApplies), re-checked by
121
+ * the server when the view loads. */
122
+ pluginViews?: PluginViewRow[];
123
+ /** Resolved theme, for the dialogs a plugin surface can open. */
124
+ theme?: ThemeMode;
125
+ /** Storage names a surface's file-chooser may span. */
126
+ storages?: string[];
99
127
  }>(),
100
128
  {
101
129
  /* ⚠ ON by default, and that is the owner's call of 2026-09-12, not a
@@ -111,6 +139,7 @@ const props = withDefaults(
111
139
  narrow: false,
112
140
  dirPerm: '',
113
141
  thumbSrc: undefined,
142
+ pluginViews: () => [],
114
143
  },
115
144
  );
116
145
 
@@ -141,7 +170,16 @@ const emit = defineEmits<{
141
170
  * is a `.tag~<name>` sentinel, how to leave whatever view is on screen to get
142
171
  * there, and what that does to the tab strip.
143
172
  */
144
- (e: 'open-tag', tag: string): void;
173
+ (e: 'open-tag', tag: string, kind: TagKind): void;
174
+ /** App plugins — an inspector view's event enqueued a job: the raw ops row. */
175
+ (e: 'plugin-op', op: Record<string, unknown>): void;
176
+ /**
177
+ * v3 §3.0 — an app's inspector screen answered "go to this file".
178
+ *
179
+ * ⚠ Carried UP with the plugin's name rather than acted on: the panel
180
+ * cannot navigate, and the explorer that can is the one holding it.
181
+ */
182
+ (e: 'plugin-open', payload: { plugin: string; req: { path: string; action?: string; view?: string } }): void;
145
183
  }>();
146
184
 
147
185
  const { t, formatSize, formatDate: formatDateOf, nodeDisplayName } = useLocale(
@@ -153,6 +191,17 @@ const single = computed<FileNode | null>(() =>
153
191
  props.nodes.length === 1 ? props.nodes[0] : null,
154
192
  );
155
193
  const isMulti = computed(() => props.nodes.length > 1);
194
+
195
+ /* App plugins — the app holding this file, in the same words the row's badge
196
+ * uses (lib/appLock). ⚠ It goes ABOVE the facts, not among them: the panel's
197
+ * "Permission: viewer" line is TRUE and misleading on its own, because the
198
+ * reason is not a permission the owner can change. */
199
+ const lockLine = computed(() => lockWords(lockOf(single.value), { t, formatDate: formatDateOf, locale: props.locale }));
200
+ /* issue #34 — a symlink the server will not follow. It goes in the SAME place
201
+ * and for the same reason as the lock above: this panel's "Size: 0 bytes" and
202
+ * "Type: file" are both true and both misleading on their own, and the reason
203
+ * is not something the person can read off any of the facts below. */
204
+ const linkNote = computed(() => linkWordsFor(single.value, { t }));
156
205
  const isFile = computed(() => single.value?.type === 'file');
157
206
  const nodeId = computed<number | null>(() =>
158
207
  typeof single.value?.id === 'number' ? (single.value.id as number) : null,
@@ -168,6 +217,15 @@ const thumb = computed<string | null>(() =>
168
217
  single.value && isFile.value && props.thumbSrc ? props.thumbSrc(single.value) : null,
169
218
  );
170
219
 
220
+ /** App plugins — the inspector views whose rule accepts the selected item. */
221
+ const pluginSections = computed<PluginViewRow[]>(() => {
222
+ const n = single.value;
223
+ if (!n || isStorageRow(n) || n.trashed === true) return [];
224
+ return (props.pluginViews ?? []).filter(
225
+ (v) => v.placement === 'inspector' && appliesToNodes(v.applies, [n], v.plugin),
226
+ );
227
+ });
228
+
171
229
  /* === gorunum:v4-dialogs — what the head says =============================
172
230
  *
173
231
  * The panel used to open with the word "Details" and put the item's own name
@@ -289,9 +347,15 @@ const effectivePerm = computed<string>(() => {
289
347
  if (typeof own === 'string' && own !== '') return own;
290
348
  return props.dirPerm || '';
291
349
  });
292
- const canManagePerms = computed(
293
- () => effectivePerm.value === 'editor' || effectivePerm.value === 'owner',
294
- );
350
+ /**
351
+ * "Manage permissions" — for an OWNER only.
352
+ *
353
+ * ⚠ It used to be offered to editors too, and an editor cannot read the grant
354
+ * list at all (`GET /api/files/permissions` answers 403): the button opened a
355
+ * dialog with no permissions in it, and the refusal was swallowed (QA,
356
+ * 2026-09-21). An editor shares from the row's "Share" like everybody else.
357
+ */
358
+ const canManagePerms = computed(() => effectivePerm.value === 'owner');
295
359
  function permLabel(level: string): string {
296
360
  return t(`inspector.perm.${level}`) === `inspector.perm.${level}`
297
361
  ? level
@@ -383,7 +447,11 @@ async function confirmRestore(v: NodeVersion): Promise<void> {
383
447
  emit('changed');
384
448
  await loadVersions(++refreshSeq);
385
449
  } catch (err) {
386
- emit('toast', (err as Error).message);
450
+ // ⚠ A 423 is an app's freeze (the signing app, while signatures are
451
+ // collected): say which app and why, in the reader's language — the
452
+ // server's own text is English and names a path.
453
+ const held = lockedRefusal(err);
454
+ emit('toast', held ? lockWords(held, { t, formatDate: formatDateOf, locale: props.locale }) : (err as Error).message);
387
455
  } finally {
388
456
  versionBusy.value = false;
389
457
  }
@@ -460,13 +528,17 @@ async function loadPeople(): Promise<void> {
460
528
  }
461
529
  }
462
530
 
531
+ /* One rule for naming a person (lib/personName). */
532
+ function grantPerson(g: Grant) {
533
+ return { name: g.user_name, display_name: g.user_display_name, email: g.user_email };
534
+ }
535
+
463
536
  function personName(g: Grant): string {
464
- return g.user_display_name || g.user_email || `#${g.user_id}`;
537
+ return nameOfPerson(grantPerson(g)) || `#${g.user_id}`;
465
538
  }
466
539
 
467
540
  function personInitial(g: Grant): string {
468
- const n = personName(g).trim();
469
- return n ? n[0].toUpperCase() : '?';
541
+ return initialOfPerson(grantPerson(g), localeTag(props.locale)) || '?';
470
542
  }
471
543
 
472
544
  /** The share-link row: the first live link, or the Create button. */
@@ -602,7 +674,7 @@ function relativeTime(iso: string): string {
602
674
  ];
603
675
  try {
604
676
  const rtf = new Intl.RelativeTimeFormat(
605
- props.locale === 'en' ? 'en' : 'tr',
677
+ localeTag(props.locale),
606
678
  { numeric: 'auto' },
607
679
  );
608
680
  for (const [unit, secs] of units) {
@@ -640,7 +712,7 @@ watch(
640
712
  <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
641
713
  <span class="fe-inspector__head-icon" aria-hidden="true" v-html="headIcon"></span>
642
714
  <div class="fe-inspector__head-text">
643
- <h2 class="fe-inspector__title" :title="headName">{{ headName }}</h2>
715
+ <h2 class="fe-inspector__title" :title="headName"><bdi>{{ headName }}</bdi></h2>
644
716
  <p class="fe-inspector__caption">{{ headCaption }}</p>
645
717
  <!-- pane:p1 — the held-subject line. Drawn ONLY while this panel is
646
718
  describing the last selected thing rather than a live selection,
@@ -703,6 +775,24 @@ watch(
703
775
  <section v-if="!tabs || tab === 'details'" class="fe-inspector__section">
704
776
  <h3 class="fe-inspector__heading">{{ t('inspector.section.general') }}</h3>
705
777
 
778
+ <!-- App plugins — an app is holding this file read-only. -->
779
+ <p v-if="lockLine" class="fe-inspector__lock" role="note" data-testid="inspector-lock">
780
+ <span aria-hidden="true">&#128274;</span>
781
+ <span><strong>{{ t('applock.inspector') }}</strong><br />{{ lockLine }}</span>
782
+ </p>
783
+
784
+ <!-- issue #34 — a link the server will not follow. -->
785
+ <p
786
+ v-if="linkNote"
787
+ class="fe-inspector__linknote"
788
+ role="note"
789
+ data-testid="inspector-symlink"
790
+ :data-link-state="linkNote.state"
791
+ >
792
+ <span aria-hidden="true">&#128279;</span>
793
+ <span><strong>{{ t('symlink.inspector') }}</strong><br />{{ linkNote.why }}</span>
794
+ </p>
795
+
706
796
  <!-- Multi selection → summary. The head already counts them, so this
707
797
  is the one fact the head does not carry. -->
708
798
  <dl v-if="isMulti" class="fe-inspector__meta">
@@ -740,7 +830,7 @@ watch(
740
830
  <div class="fe-inspector__row">
741
831
  <dt>{{ t('inspector.path') }}</dt>
742
832
  <dd class="fe-inspector__pathcell">
743
- <span class="fe-inspector__path" :title="single.path">{{ single.path }}</span>
833
+ <span class="fe-inspector__path" :title="single.path"><bdi>{{ single.path }}</bdi></span>
744
834
  <button
745
835
  type="button"
746
836
  class="fe-inspector__copy"
@@ -772,8 +862,14 @@ watch(
772
862
  technical facts about a file already are (Path, MIME, ETag), and
773
863
  it is still one click to copy. Drawn only when the row actually
774
864
  has one: a client-synthesized row (a multi-storage folder) has
775
- no backend id, and an empty "ID —" teaches nothing. -->
776
- <div v-if="typeof single.id === 'number'" class="fe-inspector__row">
865
+ no backend id, and an empty "ID —" teaches nothing.
866
+
867
+ ⚠⚠ And only for an ADMINISTRATOR (Burak, 2026-09-23): the id
868
+ is the handle admin screens take (Admin → File history, an
869
+ audit row), and it says nothing to anyone else. Here, in the
870
+ shared panel, so every surface that draws it — the explorer,
871
+ the Drive shell, an embed — answers the same way. -->
872
+ <div v-if="callerAdmin && typeof single.id === 'number'" class="fe-inspector__row">
777
873
  <dt>{{ t('inspector.nodeId') }}</dt>
778
874
  <dd class="fe-inspector__pathcell">
779
875
  <span class="fe-inspector__path">{{ single.id }}</span>
@@ -831,11 +927,32 @@ watch(
831
927
  :auth-headers="authHeaders"
832
928
  :auth-credentials="authCredentials"
833
929
  @change="(tags: string[]) => emit('tags-changed', tags)"
834
- @open="(tag: string) => emit('open-tag', tag)"
930
+ @open="(tag: string, kind: TagKind) => emit('open-tag', tag, kind) /* etiket:k2 —
931
+ the KIND travels with the name: a file can carry a personal
932
+ and a team tag of one name, and the chip that was clicked
933
+ decides which view opens. */"
835
934
  @error="() => emit('toast', t('inspector.error'))"
836
935
  />
837
936
  </section>
838
937
 
938
+ <!-- ══ App plugins — one collapsible section per matching `inspector`
939
+ view. Lazy: nothing is requested until the section is opened. -->
940
+ <template v-if="single && (!tabs || tab === 'details')">
941
+ <PluginInspectorSection
942
+ v-for="v in pluginSections"
943
+ :key="`${v.plugin}/${v.id}`"
944
+ :api="api"
945
+ :view="v"
946
+ :path="single.path"
947
+ :locale="locale"
948
+ :theme="theme"
949
+ :storages="storages"
950
+ @op="(op) => emit('plugin-op', op)"
951
+ @toast="(m) => emit('toast', m)"
952
+ @open="(req) => emit('plugin-open', { plugin: v.plugin, req })"
953
+ />
954
+ </template>
955
+
839
956
  <!-- ══ Sürümler ══ -->
840
957
  <section
841
958
  v-if="(versionsState === 'ok' || versionsState === 'error') && (!tabs || tab === 'activity')"