@brftech/filex-core 0.42.1 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +14 -2
  2. package/dist/ArchiveViewer-BqU761Wk.js +149 -0
  3. package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
  4. package/dist/CsvViewer-ByHVK5ga.js +172 -0
  5. package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
  6. package/dist/{DrawioViewer-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-DS3mb3k8.js → EpubViewer-DJwUKlHW.js} +50 -50
  9. package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
  10. package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
  11. package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
  12. package/dist/MermaidViewer-CEDTD_pp.js +136 -0
  13. package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
  14. package/dist/PsdViewer-CZcxUpZO.js +121 -0
  15. package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
  16. package/dist/TiffViewer-BwI62CbQ.js +141 -0
  17. package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
  18. package/dist/{Viewer3D-CIrOn63I.js → Viewer3D-DoeZcuMm.js} +16 -16
  19. package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
  20. package/dist/filex-core.js +475 -219
  21. package/dist/filex-core.umd.cjs +165 -108
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/fonts/caveat-latin-ext.woff2 +0 -0
  24. package/dist/fonts/caveat-latin.woff2 +0 -0
  25. package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
  26. package/dist/fonts/dancing-script-latin.woff2 +0 -0
  27. package/dist/fonts/homemade-apple-latin.woff2 +0 -0
  28. package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
  29. package/dist/fonts/source-serif-4-latin.woff2 +0 -0
  30. package/dist/index-DuWnGuv5.js +35901 -0
  31. package/dist/index-DuWnGuv5.js.map +1 -0
  32. package/dist/index.d.ts +5241 -553
  33. package/dist/pdf-CaPYsOUo.js +21302 -0
  34. package/dist/pdf-CaPYsOUo.js.map +1 -0
  35. package/dist/style.css +1 -1
  36. package/package.json +1 -1
  37. package/src/FileExplorer.vue +1207 -343
  38. package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
  39. package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
  40. package/src/assets/fonts/README.md +58 -0
  41. package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
  42. package/src/assets/fonts/caveat-latin.woff2 +0 -0
  43. package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
  44. package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
  45. package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
  46. package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
  47. package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
  48. package/src/components/AdvancedSearch.vue +49 -0
  49. package/src/components/Breadcrumb.vue +5 -5
  50. package/src/components/ChoiceButtons.vue +152 -0
  51. package/src/components/CommandPalette.vue +5 -5
  52. package/src/components/ConnectionGuideView.vue +1 -1
  53. package/src/components/ConnectionNotice.vue +59 -0
  54. package/src/components/ConnectionsPanel.vue +30 -568
  55. package/src/components/ContextMenu.vue +31 -5
  56. package/src/components/DataTable.vue +1331 -0
  57. package/src/components/FilePane.vue +3 -0
  58. package/src/components/FilterBar.vue +12 -5
  59. package/src/components/GalleryView.vue +50 -2
  60. package/src/components/GridView.vue +51 -3
  61. package/src/components/HomeView.vue +18 -6
  62. package/src/components/InspectorPanel.vue +134 -17
  63. package/src/components/ListView.vue +366 -900
  64. package/src/components/LogoMark.vue +57 -0
  65. package/src/components/NFSExportsPanel.vue +76 -28
  66. package/src/components/OnboardingTour.vue +19 -7
  67. package/src/components/OperationsCenter.vue +77 -1
  68. package/src/components/PendingOpsTray.vue +59 -5
  69. package/src/components/PresenceBar.vue +7 -7
  70. package/src/components/ProductVersion.vue +31 -0
  71. package/src/components/QuickLook.vue +10 -14
  72. package/src/components/RecentlyOpened.vue +47 -17
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -56
  76. package/src/components/SSHKeysPanel.vue +79 -26
  77. package/src/components/SideNav.vue +260 -96
  78. package/src/components/StarButton.vue +1 -0
  79. package/src/components/StorageFields.vue +74 -6
  80. package/src/components/StorageTags.vue +62 -0
  81. package/src/components/TabBar.vue +1 -1
  82. package/src/components/TagKindIcon.vue +56 -0
  83. package/src/components/TagPicker.vue +226 -40
  84. package/src/components/ThemePalette.vue +4 -4
  85. package/src/components/TimeZonePicker.vue +29 -10
  86. package/src/components/TokensPanel.vue +82 -27
  87. package/src/components/Toolbar.vue +28 -6
  88. package/src/components/plugin/PluginConfirmModal.vue +54 -0
  89. package/src/components/plugin/PluginInspectorSection.vue +149 -0
  90. package/src/components/plugin/PluginPageView.vue +339 -0
  91. package/src/components/plugin/PluginViewModal.vue +159 -0
  92. package/src/components/plugin/SurfaceConversation.vue +76 -0
  93. package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
  94. package/src/components/plugin/SurfaceRenderer.vue +299 -0
  95. package/src/components/plugin/SurfaceSections.vue +91 -0
  96. package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
  97. package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
  98. package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
  99. package/src/components/plugin/nodes/SurfaceList.vue +127 -0
  100. package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
  101. package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
  102. package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
  103. package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
  104. package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
  105. package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
  106. package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
  107. package/src/components/public/PublicLanguagePicker.vue +58 -0
  108. package/src/components/public/PublicLinkPage.vue +286 -0
  109. package/src/components/public/PublicLinkPreview.vue +85 -0
  110. package/src/components/public/PublicPinGate.vue +108 -0
  111. package/src/components/public/PublicRequestBody.vue +204 -0
  112. package/src/components/public/PublicShareBody.vue +188 -0
  113. package/src/components/public/PublicShell.vue +332 -0
  114. package/src/composables/useConnections.ts +8 -2
  115. package/src/composables/useFileApi.ts +183 -44
  116. package/src/composables/useLocale.ts +277 -41
  117. package/src/composables/useNFSExports.ts +8 -10
  118. package/src/composables/useOperations.ts +44 -3
  119. package/src/composables/usePendingOps.ts +59 -8
  120. package/src/composables/usePluginActions.ts +121 -0
  121. package/src/composables/usePluginSurface.ts +223 -0
  122. package/src/composables/usePublicBranding.ts +172 -0
  123. package/src/composables/usePublicLink.ts +573 -0
  124. package/src/composables/usePublicPage.ts +61 -0
  125. package/src/composables/useS3Keys.ts +17 -12
  126. package/src/composables/useSSHKeys.ts +8 -10
  127. package/src/composables/useTokens.ts +8 -12
  128. package/src/composables/useUploadChunked.ts +18 -8
  129. package/src/composables/useViewerFetch.ts +6 -3
  130. package/src/index.ts +404 -8
  131. package/src/lib/actionIcons.ts +46 -1
  132. package/src/lib/advSearch.ts +14 -7
  133. package/src/lib/appLock.ts +130 -0
  134. package/src/lib/connection.ts +110 -0
  135. package/src/lib/destinationTree.ts +19 -3
  136. package/src/lib/direction.ts +353 -0
  137. package/src/lib/dragGhost.ts +6 -0
  138. package/src/lib/dropLimits.ts +69 -0
  139. package/src/lib/errorWords.ts +394 -0
  140. package/src/lib/externalReach.ts +5 -1
  141. package/src/lib/fileFilters.ts +7 -2
  142. package/src/lib/fileIcons.ts +4 -0
  143. package/src/lib/internalPaths.ts +77 -0
  144. package/src/lib/listInput.ts +64 -0
  145. package/src/lib/listing.ts +58 -23
  146. package/src/lib/modalStack.ts +26 -0
  147. package/src/lib/nodeRow.ts +121 -0
  148. package/src/lib/pdfFieldRules.ts +126 -0
  149. package/src/lib/pdfFields.ts +589 -0
  150. package/src/lib/pdfFieldsGeom.ts +203 -0
  151. package/src/lib/pdfjsLoader.ts +51 -0
  152. package/src/lib/personName.ts +47 -0
  153. package/src/lib/pluginApplies.ts +123 -0
  154. package/src/lib/pluginLabel.ts +72 -0
  155. package/src/lib/pluginMenu.ts +253 -0
  156. package/src/lib/pluginPage.ts +58 -0
  157. package/src/lib/plural.ts +181 -0
  158. package/src/lib/popupLayer.ts +103 -0
  159. package/src/lib/prefs.ts +563 -0
  160. package/src/lib/productVersion.ts +35 -0
  161. package/src/lib/publicLayout.ts +39 -0
  162. package/src/lib/serviceGate.ts +104 -0
  163. package/src/lib/shareTtl.ts +25 -11
  164. package/src/lib/signFonts.ts +85 -0
  165. package/src/lib/signaturePad.ts +111 -0
  166. package/src/lib/sortOrder.ts +100 -38
  167. package/src/lib/star.ts +7 -1
  168. package/src/lib/surfaceCell.ts +41 -0
  169. package/src/lib/surfaceConditions.ts +156 -0
  170. package/src/lib/surfaceOpen.ts +106 -0
  171. package/src/lib/surfaceSteps.ts +68 -0
  172. package/src/lib/surfaceValues.ts +191 -0
  173. package/src/lib/symlink.ts +150 -0
  174. package/src/lib/tableColumns.ts +493 -0
  175. package/src/lib/tableEnv.ts +34 -0
  176. package/src/lib/tablePrefs.ts +84 -0
  177. package/src/lib/tags.ts +121 -12
  178. package/src/lib/themes.ts +286 -18
  179. package/src/lib/tour.ts +152 -0
  180. package/src/lib/trashEmpty.ts +111 -0
  181. package/src/lib/trashTimeLeft.ts +23 -0
  182. package/src/lib/uiLocales.ts +460 -0
  183. package/src/lib/viewPrefs.ts +696 -660
  184. package/src/lib/viewPrefsHttp.ts +78 -0
  185. package/src/locales/en.ts +522 -119
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +539 -155
  188. package/src/modals/ConvertModal.vue +62 -11
  189. package/src/modals/DestinationPickerModal.vue +59 -16
  190. package/src/modals/Modal.vue +113 -23
  191. package/src/modals/NewDocumentModal.vue +16 -6
  192. package/src/modals/NewFolderModal.vue +8 -0
  193. package/src/modals/PermissionsModal.vue +272 -163
  194. package/src/modals/PreviewModal.vue +119 -20
  195. package/src/modals/RenameModal.vue +37 -1
  196. package/src/styles/base.css +2895 -241
  197. package/src/styles/sign-fonts.css +118 -0
  198. package/src/styles/variables.css +60 -0
  199. package/src/types/Connections.ts +17 -0
  200. package/src/types/ExplorerConfig.ts +117 -1
  201. package/src/types/FileNode.ts +58 -0
  202. package/src/types/Plugins.ts +494 -0
  203. package/src/types/Public.ts +212 -0
  204. package/src/viewers/ArchiveViewer.vue +93 -48
  205. package/src/viewers/CsvViewer.vue +99 -53
  206. package/src/viewers/DrawioViewer.vue +35 -9
  207. package/src/viewers/EpubViewer.vue +7 -8
  208. package/src/viewers/IpynbViewer.vue +3 -1
  209. package/src/viewers/MermaidViewer.vue +4 -5
  210. package/src/viewers/PdfViewer.vue +8 -17
  211. package/src/viewers/PsdViewer.vue +2 -4
  212. package/src/viewers/TiffViewer.vue +4 -5
  213. package/src/viewers/Viewer3D.vue +1 -1
  214. package/LICENSE +0 -21
  215. package/dist/ArchiveViewer-CLNevfPE.js +0 -124
  216. package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
  217. package/dist/CsvViewer-K8XA3z7N.js +0 -141
  218. package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
  219. package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
  220. package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
  221. package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
  222. package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
  223. package/dist/MermaidViewer-DqucH_io.js +0 -137
  224. package/dist/MermaidViewer-DqucH_io.js.map +0 -1
  225. package/dist/PsdViewer-CwiVENmq.js +0 -122
  226. package/dist/PsdViewer-CwiVENmq.js.map +0 -1
  227. package/dist/TiffViewer-CpewSVmQ.js +0 -142
  228. package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
  229. package/dist/Viewer3D-CIrOn63I.js.map +0 -1
  230. package/dist/index-Dx0IbBcI.js +0 -26831
  231. package/dist/index-Dx0IbBcI.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
@@ -0,0 +1,188 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * PublicShareBody — what a share actually contains.
4
+ *
5
+ * Three bodies, one shell (`PublicShell`): a document to download, a folder,
6
+ * or an app plugin's screen when the link carries one (v3 §1.1 — "an app's
7
+ * public page IS a share"). The chrome around all three — branding, the PIN
8
+ * gate, expiry, the language picker — is the shell's and is not repeated
9
+ * here.
10
+ *
11
+ * ⚠ The surface is drawn by the SAME `SurfaceConversation` the explorer's
12
+ * dialog and an app's page use. A visitor and a signed-in user must see one
13
+ * surface rendered one way, or every plugin has to be tested twice.
14
+ *
15
+ * ⚠⚠ The bytes are NOT fetched through `/api/`. `/s/<token>` has always been
16
+ * the address that serves the file and still is: the SPA is what a
17
+ * JavaScript browser gets when it NAVIGATES there, and `?download=1` /
18
+ * `?zip=1` are the escapes that mean "the thing, not the page"
19
+ * (`handlers/public_shell.go`). A second download route under `/api/` would
20
+ * be a second thing to keep working.
21
+ */
22
+ import { computed } from 'vue';
23
+ import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
24
+ import type { PublicEntry, PublicShareInfo } from '../../types/Public';
25
+ import type { PublicLinkStore } from '../../composables/usePublicLink';
26
+ import { useLocale } from '../../composables/useLocale';
27
+ import SurfaceConversation from '../plugin/SurfaceConversation.vue';
28
+ import SurfaceFooterButtons from '../plugin/SurfaceFooterButtons.vue';
29
+
30
+ const props = defineProps<{
31
+ link: PublicLinkStore<PublicShareInfo> & {
32
+ downloadUrl: (o?: { zip?: boolean; path?: string }) => string;
33
+ noJsUrl: () => string;
34
+ };
35
+ locale: LocaleCode | string;
36
+ theme?: ThemeMode;
37
+ }>();
38
+
39
+ const emit = defineEmits<{
40
+ (e: 'navigate', path: string): void;
41
+ }>();
42
+
43
+ const { t, formatSize, formatDate } = useLocale(() => props.locale);
44
+
45
+ const info = computed(() => props.link.info.value);
46
+ const kind = computed(() => info.value?.kind ?? 'file');
47
+ const node = computed(() => info.value?.node ?? null);
48
+ const app = computed(() => info.value?.app ?? null);
49
+ const entries = computed<PublicEntry[]>(() => info.value?.entries ?? []);
50
+
51
+ /**
52
+ * A picture is shown, everything else is offered.
53
+ *
54
+ * ⚠ Deliberately narrow. A public page renders bytes a stranger was sent by
55
+ * a stranger; an <img> is a decode the browser does every day, while an
56
+ * inline frame of arbitrary content on our origin is not something a share
57
+ * link should be able to ask for.
58
+ */
59
+ const isImage = computed(() => (node.value?.mime ?? '').startsWith('image/'));
60
+
61
+ /** The path back up, or null at the share's root. */
62
+ const parent = computed(() => {
63
+ const p = (info.value?.path ?? '').replace(/\/+$/, '');
64
+ if (!p) return null;
65
+ const i = p.lastIndexOf('/');
66
+ return i <= 0 ? '' : p.slice(0, i);
67
+ });
68
+ </script>
69
+
70
+ <template>
71
+ <!-- ── an app plugin's screen ─────────────────────────────────────── -->
72
+ <template v-if="kind === 'app'">
73
+ <!-- ⚠⚠ A document in an app's screen (a signing link's "see and
74
+ approve" step) is fitted to the WINDOW, as the in-app page fits it —
75
+ never drawn at the card's full width inside a fixed-height pane.
76
+ Measured on a signing link, 2026-09-21: the page was 676px wide in a
77
+ 615px-tall pane, the signer saw the top 60% of it and none of their
78
+ own boxes, which sat at the bottom — the owner's "arayüz berbat". -->
79
+ <SurfaceConversation
80
+ :conv="link.conv"
81
+ :locale="locale as LocaleCode"
82
+ :theme="theme"
83
+ :file-url="link.resolveFile"
84
+ :toast="link.toast.value"
85
+ layout="page"
86
+ page-height="min(80vh, 1000px)"
87
+ />
88
+ <div v-if="link.conv.footer.value.length" class="fe-ppage__actions">
89
+ <SurfaceFooterButtons :conv="link.conv" :locale="locale as LocaleCode" testid-prefix="public-page" />
90
+ </div>
91
+ <!-- ⚠ The files an app exposes are drawn the way a SHARED FILE is —
92
+ its name and size, and a Download button — because to the person
93
+ holding the link that is what they are: a signer's copy of the
94
+ document is a file somebody shared with them. It used to be a
95
+ "Documents" heading over a bare link, a second way of saying the
96
+ same thing on what should be the same page. -->
97
+ <section v-if="app?.files?.length" class="fe-ppage__appfiles" data-testid="public-page-files">
98
+ <div v-for="f in app?.files ?? []" :key="f.ref" class="fe-ppage__file-one fe-ppage__appfile">
99
+ <p class="fe-surface__text">
100
+ <strong>{{ f.name }}</strong>
101
+ <span v-if="f.size" class="fe-ppage__filesize"> · {{ formatSize(f.size) }}</span>
102
+ </p>
103
+ <a
104
+ class="fe-btn"
105
+ :href="link.fileDownloadUrl(f.ref)"
106
+ download
107
+ :title="t('plugin.page.open_file', { name: f.name })"
108
+ :data-testid="`public-page-file-${f.ref}`"
109
+ >
110
+ {{ t('ctx.download') }}
111
+ </a>
112
+ </div>
113
+ </section>
114
+ </template>
115
+
116
+ <!-- ── a folder ───────────────────────────────────────────────────── -->
117
+ <template v-else-if="kind === 'folder'">
118
+ <div class="fe-ppage__file-one" data-testid="public-share-folder">
119
+ <!-- Same rule as a document: the heading is the folder's name already. -->
120
+ <p v-if="!node?.name" class="fe-surface__text">
121
+ <strong>{{ info?.subject }}</strong>
122
+ </p>
123
+ <nav v-if="info?.path" class="fe-ppage__crumbs" data-testid="public-share-crumbs">
124
+ <button type="button" class="fe-btn fe-btn--sm" @click="emit('navigate', parent ?? '')">
125
+ {{ t('public.up') }}
126
+ </button>
127
+ <span class="fe-ppage__crumb">{{ info?.path }}</span>
128
+ </nav>
129
+
130
+ <!-- ⚠ The server does not send a listing for a folder share yet: it
131
+ answers the folder's NAME and leaves the walking to the no-JS page.
132
+ The listing is drawn the moment one arrives; until then the two
133
+ things a visitor actually came for are here, and the third is one
134
+ link away. Writing a SECOND browse implementation against a
135
+ different endpoint is what v3 exists to stop. -->
136
+ <ul v-if="entries.length" class="fe-ppage__files" data-testid="public-share-entries">
137
+ <li v-for="e in entries" :key="e.path" class="fe-ppage__file" :data-dir="e.is_dir ? '1' : undefined">
138
+ <button
139
+ v-if="e.is_dir"
140
+ type="button"
141
+ class="fe-ppage__entry"
142
+ :data-testid="`public-entry-${e.name}`"
143
+ @click="emit('navigate', e.path)"
144
+ >
145
+ <bdi>{{ e.name }}</bdi>
146
+ </button>
147
+ <a
148
+ v-else
149
+ :href="e.url || link.downloadUrl({ path: e.path })"
150
+ class="fe-ppage__entry"
151
+ :data-testid="`public-entry-${e.name}`"
152
+ download
153
+ ><bdi>{{ e.name }}</bdi></a>
154
+ <span v-if="!e.is_dir && e.size !== undefined" class="fe-ppage__filesize">{{ formatSize(e.size) }}</span>
155
+ <span v-if="e.modified" class="fe-ppage__filesize">{{ formatDate(e.modified) }}</span>
156
+ </li>
157
+ </ul>
158
+
159
+ <div class="fe-ppage__actions">
160
+ <a class="fe-btn fe-btn--primary" :href="link.downloadUrl({ zip: true })" data-testid="public-share-zip">
161
+ {{ t('public.download_all') }}
162
+ </a>
163
+ <a v-if="!entries.length" class="fe-btn" :href="link.noJsUrl()" data-testid="public-share-browse">
164
+ {{ t('public.browse') }}
165
+ </a>
166
+ </div>
167
+ </div>
168
+ </template>
169
+
170
+ <!-- ── one document ───────────────────────────────────────────────── -->
171
+ <template v-else>
172
+ <div class="fe-ppage__file-one" data-testid="public-share-file">
173
+ <img v-if="isImage" class="fe-ppage__shot" :src="link.downloadUrl()" :alt="node?.name ?? ''" />
174
+ <!-- ⚠ The name is NOT repeated here. The shell's heading is already the
175
+ file's own name (`PublicLinkPage.title`), and printing it again
176
+ under itself — "brand-guidelines.pdf" over "brand-guidelines.pdf ·
177
+ 791 B" — is the stutter the levelled-down page shipped with. Only
178
+ the size, and the name as a fallback for a link that has none. -->
179
+ <p class="fe-surface__text fe-surface__text--muted" data-testid="public-share-fileline">
180
+ <strong v-if="!node?.name">{{ info?.subject }}</strong>
181
+ <span v-if="node?.size !== undefined" class="fe-ppage__filesize">{{ formatSize(node?.size) }}</span>
182
+ </p>
183
+ <a class="fe-btn fe-btn--primary" :href="link.downloadUrl()" data-testid="public-share-download">
184
+ {{ t('ctx.download') }}
185
+ </a>
186
+ </div>
187
+ </template>
188
+ </template>
@@ -0,0 +1,332 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * PublicShell — the frame every public link is drawn in.
4
+ *
5
+ * ⚠⚠ v3 §1, and the whole point of the round: a share, a file request and an
6
+ * app plugin's page are ONE page with three bodies, not three pages. What
7
+ * the shell owns is exactly what they used to each get wrong on their own —
8
+ *
9
+ * · the instance's name, logo and colour (§1.2: whitelabel means
10
+ * whitelabel — a signature request from a renamed instance does not say
11
+ * "filex");
12
+ * · the PIN gate, once, with one wording and one lockout behaviour;
13
+ * · expiry and the visit counter, said out loud rather than discovered;
14
+ * · "this link is not available", identical for expired, used up and
15
+ * withdrawn, because a visitor must not be told which;
16
+ * · the language picker, including a language an app plugin added.
17
+ *
18
+ * The BODY is a slot. Nothing in here knows whether it is showing a document,
19
+ * a folder, a drop box or a plugin's screen, and that is what keeps the
20
+ * three from drifting apart again.
21
+ *
22
+ * ⚠⚠ Unifying them had to level them UP and did not (owner, 2026-09-23:
23
+ * "böyle güzel bir ekranımız vardı share katmanında … iki sayfa aynı olsun
24
+ * dediğim için ikisini de kötü hale çevirmişsin"). The look is the one the
25
+ * Go-rendered gate had until v0.42.2 and it is the shell's, not a page's:
26
+ * the mark centred ABOVE the card, a round tinted badge, a heading, a quiet
27
+ * line, and — on a gate — a full-width field and a full-width accent button
28
+ * on a card that is 400px and centred in the viewport. A body that wants
29
+ * room asks for it through `layout`, which is the only thing that varies
30
+ * (`gate` 400 · `form` 520 · `wide` 880 — the old stylesheet's three cards).
31
+ *
32
+ * ⚠ It lives in `packages/core`, not in the web app, so the desktop app and
33
+ * an embed draw the same public page. `web/` only binds the routes.
34
+ *
35
+ * ⚠ The accent is applied to THIS element, never to `:root` — the shell can
36
+ * be mounted inside another page, and a link must not repaint its host.
37
+ */
38
+ import { computed } from 'vue';
39
+ import type { LocaleCode, ThemeMode } from '../../types/ExplorerConfig';
40
+ import type { PublicStatus } from '../../composables/usePublicLink';
41
+ import type { PublicLayout } from '../../lib/publicLayout';
42
+ import { useLocale } from '../../composables/useLocale';
43
+ import { DEFAULT_BRAND_NAME } from '../../composables/usePublicBranding';
44
+ import LogoMark from '../LogoMark.vue';
45
+ import PublicPinGate from './PublicPinGate.vue';
46
+ import PublicLanguagePicker from './PublicLanguagePicker.vue';
47
+
48
+ const props = defineProps<{
49
+ status: PublicStatus;
50
+ locale: LocaleCode | string;
51
+ /** `light` | `dark` — already resolved; `auto` is the caller's to answer. */
52
+ theme?: 'light' | 'dark' | ThemeMode;
53
+ /** The instance's name (branding, or the product's own). */
54
+ brandName: string;
55
+ logoUrl?: string;
56
+ /** `--fe-*` overrides from the instance's accent colour. */
57
+ accentStyle?: Record<string, string>;
58
+ footerText?: string;
59
+ hidePoweredBy?: boolean;
60
+ /**
61
+ * How much room the body needs (`lib/publicLayout`). Every state that is
62
+ * NOT `ready` overrides it to `gate`: a PIN box, "this link is not
63
+ * available" and "thank you" are the same short, centred card whatever
64
+ * was going to be behind them.
65
+ */
66
+ layout?: PublicLayout;
67
+ /**
68
+ * The round badge a READY body opens with — `file` or `folder`. Every
69
+ * other badge is a state of the shell's own (a padlock, a tick, a warning
70
+ * sign, a spinner) and is not the caller's to pick.
71
+ */
72
+ badge?: 'file' | 'folder' | '';
73
+ /** The heading: a file name, a folder, a plugin page's title. */
74
+ title?: string;
75
+ /** The line under it — a share message, a signature request's subject. */
76
+ subject?: string;
77
+ /** RFC 3339, or null for "does not expire". */
78
+ expiresAt?: string | null;
79
+ /** Visits left before the link stops answering; null = uncounted. */
80
+ visitsLeft?: number | null;
81
+ pinBusy?: boolean;
82
+ pinFailure?: '' | 'wrong' | 'locked';
83
+ lockMessage?: string;
84
+ /** A failure outside the conversation, in the server's words. */
85
+ failure?: string;
86
+ }>();
87
+
88
+ const emit = defineEmits<{
89
+ (e: 'pin', pin: string): void;
90
+ (e: 'retry'): void;
91
+ (e: 'update:locale', v: string): void;
92
+ }>();
93
+
94
+ // ⚠ RTL: `dir` on the page's own root too — the shell is the whole page when
95
+ // the admin app mounts it, but an embedder may mount it in a page of its own.
96
+ const { t, formatDate, dir } = useLocale(() => props.locale);
97
+
98
+ /** The body is drawn for these; everything else is a state of the shell's own. */
99
+ const showBody = computed(() => props.status === 'ready');
100
+
101
+ /** A state of the shell's own is always the narrow, centred card. */
102
+ const layout = computed<PublicLayout>(() => (showBody.value ? (props.layout ?? 'form') : 'gate'));
103
+
104
+ const themeClass = computed(() =>
105
+ props.theme === 'dark' ? 'fe--theme-dark' : props.theme === 'light' ? 'fe--theme-light' : '',
106
+ );
107
+
108
+ const expiry = computed(() => {
109
+ if (!props.expiresAt) return '';
110
+ const ms = Date.parse(props.expiresAt);
111
+ if (!Number.isFinite(ms)) return '';
112
+ return t('public.expires', { when: formatDate(ms, { time: true }) });
113
+ });
114
+
115
+ const visits = computed(() => {
116
+ const n = props.visitsLeft;
117
+ if (n === null || n === undefined || !Number.isFinite(n)) return '';
118
+ return t('public.visits_left', { n });
119
+ });
120
+
121
+ /**
122
+ * Which badge the card opens with. ONE decision, so a state and a body can
123
+ * never both draw one, and a body cannot invent a shape of its own.
124
+ */
125
+ const badgeKind = computed(() => {
126
+ if (props.status === 'pin') return 'lock';
127
+ if (props.status === 'accepted' || props.status === 'done') return 'check';
128
+ if (props.status === 'loading') return 'loading';
129
+ if (!showBody.value) return 'alert';
130
+ return props.badge ?? '';
131
+ });
132
+ const badgeTone = computed(() =>
133
+ badgeKind.value === 'check'
134
+ ? 'fe-ppage__badge--ok'
135
+ : badgeKind.value === 'alert'
136
+ ? 'fe-ppage__badge--err'
137
+ : '',
138
+ );
139
+
140
+ /**
141
+ * Whether the PRODUCT's own mark may be drawn beside the attribution line.
142
+ *
143
+ * ⚠ Only when the instance has neither a logo nor a name of its own — that
144
+ * is, when the line under it reads "Served by filex" and the folder-and-tick
145
+ * is what it is talking about. An operator who has renamed the instance gets
146
+ * their own logo, or their name and nothing else: putting our mark next to
147
+ * "Served by Acme Files" is the whitelabel leak v3 §1.2 exists to stop, and
148
+ * the shell's own test asserts a renamed page never says "filex".
149
+ *
150
+ * ⚠ The mark is `@brftech/filex-core`'s LogoMark — the ONE copy in the
151
+ * product (dup-scan: `brand-mark`). It was hand-typed here for about an hour
152
+ * on 2026-09-23 and the duplicate gate caught it, which is exactly the
153
+ * failure that once shipped the pre-rebrand indigo on the page strangers see.
154
+ */
155
+ const ownMark = computed(() => !props.logoUrl && props.brandName === DEFAULT_BRAND_NAME);
156
+
157
+ function pluginNote(plugin: string): string {
158
+ return t('public.language_from', { app: plugin });
159
+ }
160
+ </script>
161
+
162
+ <template>
163
+ <div
164
+ class="fe fe-ppage"
165
+ :dir="dir"
166
+ :class="[themeClass, `fe-ppage--${layout}`]"
167
+ :style="accentStyle"
168
+ data-testid="public-page"
169
+ :data-state="status"
170
+ :data-layout="layout"
171
+ >
172
+ <!-- ⚠ ABOVE the card and centred on it. Inside the card it read as a
173
+ caption on somebody else's document; above it, it says whose page
174
+ this is — which is the first thing a stranger needs.
175
+ ⚠ The mark is the INSTANCE's logo, or — only while the instance is
176
+ still called by the product's name — the product's own (`ownMark`).
177
+ It is DRAWN, never re-typed: LogoMark is the one home of that path
178
+ data (dup-scan `brand-mark`), and a second hand copy is how the page
179
+ strangers see shipped the pre-rebrand indigo for a whole wave. -->
180
+ <header class="fe-ppage__brand" data-testid="public-brand">
181
+ <img v-if="logoUrl" class="fe-ppage__logo" :src="logoUrl" :alt="brandName" />
182
+ <LogoMark v-else-if="ownMark" class="fe-ppage__logo" />
183
+ <span class="fe-ppage__brandname">{{ brandName }}</span>
184
+ </header>
185
+
186
+ <main class="fe-ppage__card">
187
+ <!-- The badge the card opens with. The shell draws the one its state
188
+ calls for; a READY body names `file` or `folder` and gets the same
189
+ circle in the same place, so a document, a folder and a locked
190
+ link are one family rather than three drawings. -->
191
+ <div
192
+ v-if="badgeKind"
193
+ class="fe-ppage__badge"
194
+ :class="badgeTone"
195
+ data-testid="public-page-badge"
196
+ :data-badge="badgeKind"
197
+ aria-hidden="true"
198
+ >
199
+ <span v-if="badgeKind === 'loading'" class="fe-ppage__spinner"></span>
200
+ <svg
201
+ v-else
202
+ viewBox="0 0 24 24"
203
+ fill="none"
204
+ stroke="currentColor"
205
+ :stroke-width="badgeKind === 'check' ? 2 : 1.8"
206
+ stroke-linecap="round"
207
+ stroke-linejoin="round"
208
+ >
209
+ <template v-if="badgeKind === 'lock'">
210
+ <rect x="4.5" y="10.5" width="15" height="9.5" rx="2" />
211
+ <path d="M8 10.5V7a4 4 0 0 1 8 0v3.5" />
212
+ <path d="M12 14.5v2" />
213
+ </template>
214
+ <template v-else-if="badgeKind === 'check'">
215
+ <path d="M4.5 12.5l5 5 10-11" />
216
+ </template>
217
+ <template v-else-if="badgeKind === 'alert'">
218
+ <path d="M10.3 4.2 2.6 17.9a2 2 0 0 0 1.7 3h15.4a2 2 0 0 0 1.7-3L13.7 4.2a2 2 0 0 0-3.4 0z" />
219
+ <path d="M12 9.5v4.5" />
220
+ <path d="M12 17.4h.01" />
221
+ </template>
222
+ <template v-else-if="badgeKind === 'folder'">
223
+ <path d="M3.5 7a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2V7z" />
224
+ <path d="M12 10.5V16" />
225
+ <path d="M9.5 13.5 12 16l2.5-2.5" />
226
+ </template>
227
+ <template v-else>
228
+ <path d="M7 3.5h7l4 4V20a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 6 20V5A1.5 1.5 0 0 1 7.5 3.5z" />
229
+ <path d="M13.5 3.5V8H18" />
230
+ <path d="M12 11.5V17" />
231
+ <path d="M9.5 14.5 12 17l2.5-2.5" />
232
+ </template>
233
+ </svg>
234
+ </div>
235
+
236
+ <!-- ⚠⚠ `&& showBody`, on purpose and confirmed 2026-09-23: the heading
237
+ is drawn only once the link has OPENED. A locked gate used to print
238
+ the file's name above the PIN box, which tells somebody who cannot
239
+ get in what is behind it — and a stranger typing at a token they
240
+ were not given learns the name of a real document. It is the same
241
+ reasoning as the one sentence for expired / used up / withdrawn
242
+ below: the page says nothing about what it is guarding until it is
243
+ satisfied the person may see it. Do not "restore" the heading here;
244
+ if a gate ever needs to name its subject, that is a decision about
245
+ what the SERVER sends before an unlock, not a template change. -->
246
+ <header v-if="(title || subject) && showBody" class="fe-ppage__head">
247
+ <h1 v-if="title" class="fe-ppage__title" data-testid="public-page-title"><bdi>{{ title }}</bdi></h1>
248
+ <p v-if="subject" class="fe-ppage__subject" data-testid="public-page-subject">{{ subject }}</p>
249
+ </header>
250
+
251
+ <!-- ⚠ Said out loud, not discovered. A link that stops working
252
+ tomorrow, or after one more visit, is a fact the person holding it
253
+ needs BEFORE they put it aside for the weekend. -->
254
+ <p v-if="(expiry || visits) && showBody" class="fe-ppage__meta" data-testid="public-page-meta">
255
+ <span v-if="expiry">{{ expiry }}</span>
256
+ <span v-if="expiry && visits" aria-hidden="true">·</span>
257
+ <span v-if="visits">{{ visits }}</span>
258
+ </p>
259
+
260
+ <p
261
+ v-if="status === 'loading'"
262
+ class="fe-surface__text fe-surface__text--muted"
263
+ data-testid="public-page-loading"
264
+ >
265
+ {{ t('plugin.page.loading') }}
266
+ </p>
267
+
268
+ <PublicPinGate
269
+ v-else-if="status === 'pin'"
270
+ :locale="locale"
271
+ :busy="pinBusy"
272
+ :failure="pinFailure"
273
+ :lock-message="lockMessage"
274
+ @submit="(p: string) => emit('pin', p)"
275
+ />
276
+
277
+ <slot v-else-if="showBody" />
278
+
279
+ <div
280
+ v-else-if="status === 'accepted'"
281
+ class="fe-ppage__state fe-ppage__state--ok"
282
+ data-testid="public-page-accepted"
283
+ >
284
+ <slot name="accepted">
285
+ <h2 class="fe-ppage__state-title">{{ t('plugin.page.accepted_title') }}</h2>
286
+ <p class="fe-surface__text">{{ t('plugin.page.accepted_text') }}</p>
287
+ </slot>
288
+ </div>
289
+
290
+ <div v-else-if="status === 'done'" class="fe-ppage__state fe-ppage__state--ok" data-testid="public-page-done">
291
+ <slot name="done">
292
+ <h2 class="fe-ppage__state-title">{{ t('plugin.page.done_title') }}</h2>
293
+ <p class="fe-surface__text">{{ t('plugin.page.done_text') }}</p>
294
+ </slot>
295
+ </div>
296
+
297
+ <div v-else-if="status === 'error'" class="fe-ppage__state" data-testid="public-page-error">
298
+ <h2 class="fe-ppage__state-title">{{ t('plugin.page.error_title') }}</h2>
299
+ <p v-if="failure" class="fe-ppage__error">{{ failure }}</p>
300
+ <button type="button" class="fe-btn fe-btn--primary" @click="emit('retry')">
301
+ {{ t('plugin.page.retry') }}
302
+ </button>
303
+ </div>
304
+
305
+ <!-- ⚠ ONE sentence for expired, used up and withdrawn. Telling a
306
+ visitor WHICH confirms that the link existed, which is more than
307
+ somebody guessing tokens should learn. -->
308
+ <div v-else class="fe-ppage__state" data-testid="public-page-unavailable">
309
+ <h2 class="fe-ppage__state-title">{{ t('plugin.page.unavailable_title') }}</h2>
310
+ <p class="fe-surface__text fe-surface__text--muted">{{ t('plugin.page.unavailable_text') }}</p>
311
+ </div>
312
+
313
+ <slot name="extra" />
314
+ </main>
315
+
316
+ <footer class="fe-ppage__foot">
317
+ <PublicLanguagePicker
318
+ :model-value="String(locale)"
319
+ :plugin-note="pluginNote"
320
+ @update:model-value="(v: string) => emit('update:locale', v)"
321
+ />
322
+ <p v-if="footerText" class="fe-ppage__footline" data-testid="public-footer-text">{{ footerText }}</p>
323
+ <!-- The modest brand line the old page ended on: a small mark and one
324
+ sentence, centred under the card. -->
325
+ <span v-if="!hidePoweredBy" class="fe-ppage__footmark" data-testid="public-footer-powered">
326
+ <img v-if="logoUrl" :src="logoUrl" alt="" />
327
+ <LogoMark v-else-if="ownMark" />
328
+ {{ t('public.served_by', { name: brandName }) }}
329
+ </span>
330
+ </footer>
331
+ </div>
332
+ </template>
@@ -32,6 +32,7 @@ import type {
32
32
  StorageWrite,
33
33
  } from '../types/Connections';
34
34
  import { useFileApi } from './useFileApi';
35
+ import { serverWords } from '../lib/errorWords';
35
36
 
36
37
  /**
37
38
  * The URL prefix the `/api/...` routes hang off.
@@ -108,9 +109,14 @@ export function useConnections(config: ExplorerConfig) {
108
109
  return (e as { status?: number } | null)?.status;
109
110
  }
110
111
 
112
+ /** A refusal, said — lib/errorWords `serverWords`, the one rule every
113
+ * connection panel shares (it used to print the server's `error` field as
114
+ * it came: an environment variable, to a regular user). ⚠ The locale is
115
+ * passed because what comes back may be the SERVER's own sentence: it
116
+ * never met a translator, and an Arabic panel needs its machine runs
117
+ * isolated (lib/direction `foreignText`). */
111
118
  function messageOf(e: unknown): string {
112
- const err = e as { message?: string } | null;
113
- return err?.message || String(e);
119
+ return serverWords(e, config.locale);
114
120
  }
115
121
 
116
122
  /** The full picture, in as few round-trips as the permission allows. */