@brftech/filex-core 0.42.2 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +14 -2
  2. package/dist/ArchiveViewer-BqU761Wk.js +149 -0
  3. package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
  4. package/dist/CsvViewer-ByHVK5ga.js +172 -0
  5. package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
  6. package/dist/{DrawioViewer-CC6rJXeP.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-CAoTSXTO.js → EpubViewer-DJwUKlHW.js} +50 -50
  9. package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
  10. package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
  11. package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
  12. package/dist/MermaidViewer-CEDTD_pp.js +136 -0
  13. package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
  14. package/dist/PsdViewer-CZcxUpZO.js +121 -0
  15. package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
  16. package/dist/TiffViewer-BwI62CbQ.js +141 -0
  17. package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
  18. package/dist/{Viewer3D-CdZeK4j4.js → Viewer3D-DoeZcuMm.js} +16 -16
  19. package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
  20. package/dist/filex-core.js +475 -219
  21. package/dist/filex-core.umd.cjs +165 -108
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/fonts/caveat-latin-ext.woff2 +0 -0
  24. package/dist/fonts/caveat-latin.woff2 +0 -0
  25. package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
  26. package/dist/fonts/dancing-script-latin.woff2 +0 -0
  27. package/dist/fonts/homemade-apple-latin.woff2 +0 -0
  28. package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
  29. package/dist/fonts/source-serif-4-latin.woff2 +0 -0
  30. package/dist/index-DuWnGuv5.js +35901 -0
  31. package/dist/index-DuWnGuv5.js.map +1 -0
  32. package/dist/index.d.ts +5227 -553
  33. package/dist/pdf-CaPYsOUo.js +21302 -0
  34. package/dist/pdf-CaPYsOUo.js.map +1 -0
  35. package/dist/style.css +1 -1
  36. package/package.json +1 -1
  37. package/src/FileExplorer.vue +1055 -253
  38. package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
  39. package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
  40. package/src/assets/fonts/README.md +58 -0
  41. package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
  42. package/src/assets/fonts/caveat-latin.woff2 +0 -0
  43. package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
  44. package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
  45. package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
  46. package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
  47. package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
  48. package/src/components/AdvancedSearch.vue +49 -0
  49. package/src/components/Breadcrumb.vue +5 -5
  50. package/src/components/ChoiceButtons.vue +152 -0
  51. package/src/components/CommandPalette.vue +5 -5
  52. package/src/components/ConnectionGuideView.vue +1 -1
  53. package/src/components/ConnectionNotice.vue +59 -0
  54. package/src/components/ConnectionsPanel.vue +30 -568
  55. package/src/components/ContextMenu.vue +31 -5
  56. package/src/components/DataTable.vue +1331 -0
  57. package/src/components/FilePane.vue +3 -0
  58. package/src/components/FilterBar.vue +12 -5
  59. package/src/components/GalleryView.vue +50 -2
  60. package/src/components/GridView.vue +51 -3
  61. package/src/components/HomeView.vue +18 -6
  62. package/src/components/InspectorPanel.vue +134 -17
  63. package/src/components/ListView.vue +366 -900
  64. package/src/components/LogoMark.vue +57 -0
  65. package/src/components/NFSExportsPanel.vue +76 -28
  66. package/src/components/OnboardingTour.vue +19 -7
  67. package/src/components/OperationsCenter.vue +77 -1
  68. package/src/components/PendingOpsTray.vue +59 -5
  69. package/src/components/PresenceBar.vue +7 -7
  70. package/src/components/ProductVersion.vue +31 -0
  71. package/src/components/QuickLook.vue +10 -14
  72. package/src/components/RecentlyOpened.vue +31 -15
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -62
  76. package/src/components/SSHKeysPanel.vue +79 -30
  77. package/src/components/SideNav.vue +260 -96
  78. package/src/components/StarButton.vue +1 -0
  79. package/src/components/StorageFields.vue +74 -6
  80. package/src/components/StorageTags.vue +62 -0
  81. package/src/components/TabBar.vue +1 -1
  82. package/src/components/TagKindIcon.vue +56 -0
  83. package/src/components/TagPicker.vue +226 -40
  84. package/src/components/ThemePalette.vue +4 -4
  85. package/src/components/TimeZonePicker.vue +29 -10
  86. package/src/components/TokensPanel.vue +82 -31
  87. package/src/components/Toolbar.vue +28 -6
  88. package/src/components/plugin/PluginConfirmModal.vue +54 -0
  89. package/src/components/plugin/PluginInspectorSection.vue +149 -0
  90. package/src/components/plugin/PluginPageView.vue +339 -0
  91. package/src/components/plugin/PluginViewModal.vue +159 -0
  92. package/src/components/plugin/SurfaceConversation.vue +76 -0
  93. package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
  94. package/src/components/plugin/SurfaceRenderer.vue +299 -0
  95. package/src/components/plugin/SurfaceSections.vue +91 -0
  96. package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
  97. package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
  98. package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
  99. package/src/components/plugin/nodes/SurfaceList.vue +127 -0
  100. package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
  101. package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
  102. package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
  103. package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
  104. package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
  105. package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
  106. package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
  107. package/src/components/public/PublicLanguagePicker.vue +58 -0
  108. package/src/components/public/PublicLinkPage.vue +286 -0
  109. package/src/components/public/PublicLinkPreview.vue +85 -0
  110. package/src/components/public/PublicPinGate.vue +108 -0
  111. package/src/components/public/PublicRequestBody.vue +204 -0
  112. package/src/components/public/PublicShareBody.vue +188 -0
  113. package/src/components/public/PublicShell.vue +332 -0
  114. package/src/composables/useConnections.ts +8 -2
  115. package/src/composables/useFileApi.ts +183 -44
  116. package/src/composables/useLocale.ts +277 -41
  117. package/src/composables/useNFSExports.ts +8 -10
  118. package/src/composables/useOperations.ts +44 -3
  119. package/src/composables/usePendingOps.ts +59 -8
  120. package/src/composables/usePluginActions.ts +121 -0
  121. package/src/composables/usePluginSurface.ts +223 -0
  122. package/src/composables/usePublicBranding.ts +172 -0
  123. package/src/composables/usePublicLink.ts +573 -0
  124. package/src/composables/usePublicPage.ts +61 -0
  125. package/src/composables/useS3Keys.ts +17 -12
  126. package/src/composables/useSSHKeys.ts +8 -10
  127. package/src/composables/useTokens.ts +8 -12
  128. package/src/composables/useUploadChunked.ts +18 -8
  129. package/src/composables/useViewerFetch.ts +6 -3
  130. package/src/index.ts +404 -8
  131. package/src/lib/actionIcons.ts +46 -1
  132. package/src/lib/advSearch.ts +14 -7
  133. package/src/lib/appLock.ts +130 -0
  134. package/src/lib/connection.ts +110 -0
  135. package/src/lib/destinationTree.ts +19 -3
  136. package/src/lib/direction.ts +353 -0
  137. package/src/lib/dragGhost.ts +6 -0
  138. package/src/lib/dropLimits.ts +69 -0
  139. package/src/lib/errorWords.ts +394 -0
  140. package/src/lib/externalReach.ts +5 -1
  141. package/src/lib/fileFilters.ts +7 -2
  142. package/src/lib/fileIcons.ts +4 -0
  143. package/src/lib/internalPaths.ts +77 -0
  144. package/src/lib/listInput.ts +64 -0
  145. package/src/lib/listing.ts +58 -23
  146. package/src/lib/modalStack.ts +26 -0
  147. package/src/lib/nodeRow.ts +18 -3
  148. package/src/lib/pdfFieldRules.ts +126 -0
  149. package/src/lib/pdfFields.ts +589 -0
  150. package/src/lib/pdfFieldsGeom.ts +203 -0
  151. package/src/lib/pdfjsLoader.ts +51 -0
  152. package/src/lib/personName.ts +47 -0
  153. package/src/lib/pluginApplies.ts +123 -0
  154. package/src/lib/pluginLabel.ts +72 -0
  155. package/src/lib/pluginMenu.ts +253 -0
  156. package/src/lib/pluginPage.ts +58 -0
  157. package/src/lib/plural.ts +181 -0
  158. package/src/lib/popupLayer.ts +103 -0
  159. package/src/lib/prefs.ts +563 -0
  160. package/src/lib/productVersion.ts +35 -0
  161. package/src/lib/publicLayout.ts +39 -0
  162. package/src/lib/serviceGate.ts +104 -0
  163. package/src/lib/shareTtl.ts +25 -11
  164. package/src/lib/signFonts.ts +85 -0
  165. package/src/lib/signaturePad.ts +111 -0
  166. package/src/lib/sortOrder.ts +100 -38
  167. package/src/lib/star.ts +7 -1
  168. package/src/lib/surfaceCell.ts +41 -0
  169. package/src/lib/surfaceConditions.ts +156 -0
  170. package/src/lib/surfaceOpen.ts +106 -0
  171. package/src/lib/surfaceSteps.ts +68 -0
  172. package/src/lib/surfaceValues.ts +191 -0
  173. package/src/lib/symlink.ts +150 -0
  174. package/src/lib/tableColumns.ts +493 -0
  175. package/src/lib/tableEnv.ts +34 -0
  176. package/src/lib/tablePrefs.ts +84 -0
  177. package/src/lib/tags.ts +121 -12
  178. package/src/lib/themes.ts +286 -18
  179. package/src/lib/tour.ts +152 -0
  180. package/src/lib/trashEmpty.ts +111 -0
  181. package/src/lib/trashTimeLeft.ts +23 -0
  182. package/src/lib/uiLocales.ts +460 -0
  183. package/src/lib/viewPrefs.ts +696 -660
  184. package/src/lib/viewPrefsHttp.ts +78 -0
  185. package/src/locales/en.ts +515 -117
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +536 -152
  188. package/src/modals/ConvertModal.vue +62 -11
  189. package/src/modals/DestinationPickerModal.vue +59 -16
  190. package/src/modals/Modal.vue +113 -23
  191. package/src/modals/NewDocumentModal.vue +16 -6
  192. package/src/modals/NewFolderModal.vue +8 -0
  193. package/src/modals/PermissionsModal.vue +272 -163
  194. package/src/modals/PreviewModal.vue +119 -20
  195. package/src/modals/RenameModal.vue +37 -1
  196. package/src/styles/base.css +2890 -260
  197. package/src/styles/sign-fonts.css +118 -0
  198. package/src/styles/variables.css +60 -0
  199. package/src/types/Connections.ts +17 -0
  200. package/src/types/ExplorerConfig.ts +117 -1
  201. package/src/types/FileNode.ts +50 -0
  202. package/src/types/Plugins.ts +494 -0
  203. package/src/types/Public.ts +212 -0
  204. package/src/viewers/ArchiveViewer.vue +93 -48
  205. package/src/viewers/CsvViewer.vue +99 -53
  206. package/src/viewers/DrawioViewer.vue +35 -9
  207. package/src/viewers/EpubViewer.vue +7 -8
  208. package/src/viewers/IpynbViewer.vue +3 -1
  209. package/src/viewers/MermaidViewer.vue +4 -5
  210. package/src/viewers/PdfViewer.vue +8 -17
  211. package/src/viewers/PsdViewer.vue +2 -4
  212. package/src/viewers/TiffViewer.vue +4 -5
  213. package/src/viewers/Viewer3D.vue +1 -1
  214. package/LICENSE +0 -21
  215. package/dist/ArchiveViewer-BUQWLhwX.js +0 -124
  216. package/dist/ArchiveViewer-BUQWLhwX.js.map +0 -1
  217. package/dist/CsvViewer-Bg2_aqzc.js +0 -141
  218. package/dist/CsvViewer-Bg2_aqzc.js.map +0 -1
  219. package/dist/DrawioViewer-CC6rJXeP.js.map +0 -1
  220. package/dist/EpubViewer-CAoTSXTO.js.map +0 -1
  221. package/dist/IpynbViewer-D0GwGpnE.js +0 -184
  222. package/dist/IpynbViewer-D0GwGpnE.js.map +0 -1
  223. package/dist/MermaidViewer-DGwAvRZg.js +0 -137
  224. package/dist/MermaidViewer-DGwAvRZg.js.map +0 -1
  225. package/dist/PsdViewer-DhGweA0m.js +0 -122
  226. package/dist/PsdViewer-DhGweA0m.js.map +0 -1
  227. package/dist/TiffViewer-D0AurvAh.js +0 -142
  228. package/dist/TiffViewer-D0AurvAh.js.map +0 -1
  229. package/dist/Viewer3D-CdZeK4j4.js.map +0 -1
  230. package/dist/index-BArI-B1M.js +0 -26911
  231. package/dist/index-BArI-B1M.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
  234. package/src/composables/useScrolledX.ts +0 -21
@@ -0,0 +1,213 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SurfacePeoplePicker — a plugin surface's `people-picker` node: who a
4
+ * document goes to. Chips for the people already chosen, one box to add
5
+ * the next.
6
+ *
7
+ * Internal users come from `GET /api/files/plugins/users?plugin=&q=`, which
8
+ * answers only for a plugin that holds `users:lookup`. A 403 (or a 404 from
9
+ * a server without the route) turns the lookup off for the life of the
10
+ * node and the box becomes plain e-mail entry — the contract's fallback,
11
+ * and the reason the box never says "no results" for a refusal.
12
+ *
13
+ * Free e-mail entry is offered when the plugin allows external people OR
14
+ * the lookup is off: with neither a way to name a user nor a way to type
15
+ * one, the node would be a wall.
16
+ */
17
+ import { computed, onBeforeUnmount, ref, watch } from 'vue';
18
+ import type { LocaleCode } from '../../../types/ExplorerConfig';
19
+ import type { FileApi } from '../../../composables/useFileApi';
20
+ import type { PluginPerson } from '../../../types/Plugins';
21
+ import { useLocale } from '../../../composables/useLocale';
22
+ import { personName } from '../../../lib/personName';
23
+ import { looksLikeEmail } from '../../../lib/surfaceValues';
24
+ import { isListSeparatorKey } from '../../../lib/listInput';
25
+
26
+ const props = defineProps<{
27
+ id: string;
28
+ modelValue: PluginPerson[];
29
+ multi?: boolean;
30
+ allowExternal?: boolean;
31
+ /** Absent: no lookup at all — free e-mail entry only. */
32
+ api?: Pick<FileApi, 'pluginUsers'>;
33
+ /** The plugin the lookup is asked for. */
34
+ plugin?: string;
35
+ locale: LocaleCode;
36
+ disabled?: boolean;
37
+ invalid?: boolean;
38
+ }>();
39
+
40
+ const emit = defineEmits<{
41
+ (e: 'update:modelValue', v: PluginPerson[]): void;
42
+ }>();
43
+
44
+ const { t } = useLocale(() => props.locale);
45
+
46
+ const query = ref('');
47
+ const suggestions = ref<PluginPerson[]>([]);
48
+ const showSuggest = ref(false);
49
+ /** The server refused (403) or has no route (404): free e-mail entry only. */
50
+ const lookupOff = ref(!props.api || !props.plugin);
51
+ const notice = ref('');
52
+ let timer: ReturnType<typeof setTimeout> | null = null;
53
+ let lastQuery = 0;
54
+
55
+ const people = computed(() => props.modelValue ?? []);
56
+ const freeEntry = computed(() => props.allowExternal === true || lookupOff.value);
57
+ const full = computed(() => props.multi !== true && people.value.length >= 1);
58
+
59
+ /* One rule for naming a person (lib/personName). */
60
+ function personLabel(p: PluginPerson): string {
61
+ return personName({ name: p.name, email: p.email });
62
+ }
63
+
64
+ function add(p: PluginPerson) {
65
+ const email = String(p.email ?? '').trim().toLowerCase();
66
+ if (!email) return;
67
+ const rest = props.multi === true ? people.value.filter((x) => x.email.toLowerCase() !== email) : [];
68
+ emit('update:modelValue', [...rest, { ...p, email }]);
69
+ query.value = '';
70
+ suggestions.value = [];
71
+ showSuggest.value = false;
72
+ notice.value = '';
73
+ }
74
+
75
+ function remove(email: string) {
76
+ emit('update:modelValue', people.value.filter((x) => x.email !== email));
77
+ }
78
+
79
+ function onInput() {
80
+ notice.value = '';
81
+ const q = query.value.trim();
82
+ if (timer) clearTimeout(timer);
83
+ if (!q || lookupOff.value) {
84
+ suggestions.value = [];
85
+ showSuggest.value = false;
86
+ return;
87
+ }
88
+ timer = setTimeout(() => void search(q), 180);
89
+ }
90
+
91
+ async function search(q: string) {
92
+ if (!props.api || !props.plugin || lookupOff.value) return;
93
+ const mine = ++lastQuery;
94
+ try {
95
+ const r = await props.api.pluginUsers(props.plugin, q);
96
+ if (mine !== lastQuery) return;
97
+ suggestions.value = (r.users ?? []).filter((u) => u && typeof u.email === 'string');
98
+ showSuggest.value = suggestions.value.length > 0;
99
+ } catch (e) {
100
+ if (mine !== lastQuery) return;
101
+ const status = (e as { status?: number })?.status;
102
+ if (status === 403 || status === 404) lookupOff.value = true;
103
+ suggestions.value = [];
104
+ showSuggest.value = false;
105
+ }
106
+ }
107
+
108
+ /** Enter / comma: the typed address, when free entry is allowed. */
109
+ function commit() {
110
+ const q = query.value.trim();
111
+ if (!q) return;
112
+ if (!freeEntry.value) {
113
+ notice.value = t('plugin.people.external_off');
114
+ return;
115
+ }
116
+ if (!looksLikeEmail(q)) {
117
+ notice.value = t('plugin.people.invalid_email');
118
+ return;
119
+ }
120
+ add({ email: q });
121
+ }
122
+
123
+ function onKeydown(ev: KeyboardEvent) {
124
+ // ⚠ "،" / "," commit too (lib/listInput — an Arabic or CJK keyboard).
125
+ if (ev.key === 'Enter' || isListSeparatorKey(ev.key)) {
126
+ ev.preventDefault();
127
+ commit();
128
+ } else if (ev.key === 'Backspace' && query.value === '' && people.value.length) {
129
+ remove(people.value[people.value.length - 1].email);
130
+ } else if (ev.key === 'Escape') {
131
+ showSuggest.value = false;
132
+ }
133
+ }
134
+
135
+ function onBlur() {
136
+ // Let a mousedown on a suggestion land first.
137
+ setTimeout(() => {
138
+ showSuggest.value = false;
139
+ }, 120);
140
+ }
141
+
142
+ watch(
143
+ () => [props.api, props.plugin] as const,
144
+ ([api, plugin]) => {
145
+ if (api && plugin) lookupOff.value = false;
146
+ },
147
+ );
148
+
149
+ onBeforeUnmount(() => {
150
+ if (timer) clearTimeout(timer);
151
+ });
152
+ </script>
153
+
154
+ <template>
155
+ <div class="fe-speople" :class="{ 'is-invalid': invalid }" data-testid="surface-people">
156
+ <ul v-if="people.length" class="fe-speople__chips" data-testid="surface-people-chips">
157
+ <li v-for="p in people" :key="p.email" class="fe-speople__chip" :title="p.email">
158
+ <span class="fe-speople__av" aria-hidden="true">{{ personLabel(p).charAt(0).toUpperCase() }}</span>
159
+ <span class="fe-speople__name">{{ personLabel(p) }}</span>
160
+ <button
161
+ type="button"
162
+ class="fe-speople__remove"
163
+ :disabled="disabled"
164
+ :title="t('plugin.people.remove', { name: personLabel(p) })"
165
+ :aria-label="t('plugin.people.remove', { name: personLabel(p) })"
166
+ :data-testid="`surface-people-remove-${p.email}`"
167
+ @click="remove(p.email)"
168
+ >
169
+ <svg class="fe-ficon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false">
170
+ <path d="M6 6l12 12M18 6L6 18" />
171
+ </svg>
172
+ </button>
173
+ </li>
174
+ </ul>
175
+ <div v-if="!full" class="fe-speople__addwrap">
176
+ <input
177
+ v-model="query"
178
+ class="fe-cfield__input fe-speople__input"
179
+ type="email"
180
+ autocomplete="off"
181
+ spellcheck="false"
182
+ :placeholder="lookupOff ? t('plugin.people.placeholder_email') : t('plugin.people.placeholder')"
183
+ :disabled="disabled"
184
+ :aria-invalid="invalid ? 'true' : undefined"
185
+ data-testid="surface-people-input"
186
+ @input="onInput"
187
+ @keydown="onKeydown"
188
+ @focus="onInput"
189
+ @blur="onBlur"
190
+ />
191
+ <button
192
+ v-if="freeEntry"
193
+ type="button"
194
+ class="fe-btn fe-btn--sm"
195
+ :disabled="disabled || !query.trim()"
196
+ data-testid="surface-people-add"
197
+ @click="commit"
198
+ >
199
+ {{ t('plugin.people.add') }}
200
+ </button>
201
+ <ul v-if="showSuggest" class="fe-share__suggest fe-speople__suggest" data-testid="surface-people-suggest">
202
+ <li v-for="u in suggestions" :key="u.user_id ?? u.email" @mousedown.prevent="add(u)">
203
+ <span class="fe-share__av fe-share__av--sm">{{ personLabel(u).charAt(0).toUpperCase() }}</span>
204
+ <span class="fe-share__suggesttxt">
205
+ <span class="fe-share__suggestname">{{ personLabel(u) }}</span>
206
+ <span class="fe-share__suggestmeta">{{ u.email }}</span>
207
+ </span>
208
+ </li>
209
+ </ul>
210
+ </div>
211
+ <p v-if="notice" class="fe-surface__error" role="alert">{{ notice }}</p>
212
+ </div>
213
+ </template>
@@ -0,0 +1,57 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SurfacePinInput — a plugin surface's `pin-input` node: a short code, 4..8
4
+ * characters, into `data.values[id]`. One input with the browser's one-time
5
+ * code affordances rather than N boxes: paste works, screen readers read one
6
+ * field, and the length is enforced by the same line that draws it.
7
+ */
8
+ import { computed } from 'vue';
9
+ import type { LocaleCode } from '../../../types/ExplorerConfig';
10
+ import { useLocale } from '../../../composables/useLocale';
11
+ import { pinLength } from '../../../lib/surfaceValues';
12
+
13
+ const props = defineProps<{
14
+ id: string;
15
+ length?: number | unknown;
16
+ modelValue: string;
17
+ locale: LocaleCode;
18
+ disabled?: boolean;
19
+ invalid?: boolean;
20
+ }>();
21
+
22
+ const emit = defineEmits<{
23
+ (e: 'update:modelValue', v: string): void;
24
+ }>();
25
+
26
+ const { t } = useLocale(() => props.locale);
27
+
28
+ const len = computed(() => pinLength(props.length));
29
+
30
+ function onInput(ev: Event) {
31
+ const raw = (ev.target as HTMLInputElement).value.replace(/\s+/g, '').slice(0, len.value);
32
+ (ev.target as HTMLInputElement).value = raw;
33
+ emit('update:modelValue', raw);
34
+ }
35
+ </script>
36
+
37
+ <template>
38
+ <div class="fe-spin" data-testid="surface-pin">
39
+ <label class="fe-cfield__label" :for="`fe-spin-${id}`">{{ t('plugin.pin.label') }}</label>
40
+ <input
41
+ :id="`fe-spin-${id}`"
42
+ class="fe-cfield__input fe-cfield__input--mono fe-spin__input"
43
+ :class="{ 'is-invalid': invalid }"
44
+ type="text"
45
+ inputmode="numeric"
46
+ autocomplete="one-time-code"
47
+ spellcheck="false"
48
+ :maxlength="len"
49
+ :size="len"
50
+ :value="modelValue"
51
+ :disabled="disabled"
52
+ :aria-invalid="invalid ? 'true' : undefined"
53
+ :style="{ '--spin-len': len }"
54
+ @input="onInput"
55
+ />
56
+ </div>
57
+ </template>
@@ -0,0 +1,119 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SurfacePreview — a plugin surface's `preview` node: the explorer's own
4
+ * look at a storage file. The bytes come through `api.fetchBlob`, the same
5
+ * authenticated preview fetch every rich viewer uses (bearer / CSRF /
6
+ * credentials mode all honoured), so an embed on another origin sees the
7
+ * picture too. Pictures, video and audio are shown as themselves; anything
8
+ * else gets the listing's own type tile and the name.
9
+ *
10
+ * ⚠ No `<img src="/api/…">` for a PATH: a plain URL cannot carry the auth
11
+ * header and resolves against the HOST page's origin in an embed (see
12
+ * useThumbs). A public page's exposed copy (`ref`, M3) is the exception by
13
+ * design: there is no auth header, the PIN cookie rides with the same-origin
14
+ * URL the host's `fileUrl` resolver hands out, and the browser's own
15
+ * viewer draws it (`<img>` for a picture, `<embed>` for a PDF or anything
16
+ * else) — so the page never buffers a document in memory.
17
+ */
18
+ import { computed, onBeforeUnmount, ref, watch } from 'vue';
19
+ import type { LocaleCode } from '../../../types/ExplorerConfig';
20
+ import type { FileApi } from '../../../composables/useFileApi';
21
+ import type { FileRefResolver } from '../../../types/Plugins';
22
+ import { useLocale } from '../../../composables/useLocale';
23
+ import { labelOfWire } from '../../../lib/destinationTree';
24
+ import { fileIconTile } from '../../../lib/fileIcons';
25
+
26
+ const props = defineProps<{
27
+ /** Adapter-qualified path of a storage file. */
28
+ path: string;
29
+ /** A public page's exposed copy (`pub:N`), shown from `fileUrl(ref)`. */
30
+ fileRef?: string;
31
+ fileUrl?: FileRefResolver;
32
+ locale: LocaleCode;
33
+ /** Absent: the node says the preview is unavailable. */
34
+ api?: Pick<FileApi, 'fetchBlob'>;
35
+ }>();
36
+
37
+ const { t } = useLocale(() => props.locale);
38
+
39
+ const url = ref<string | null>(null);
40
+ const mime = ref('');
41
+ const state = ref<'loading' | 'ok' | 'error'>('loading');
42
+ let seq = 0;
43
+
44
+ /** The exposed copy, when the node names one and the host can resolve it. */
45
+ const exposed = computed(() => (props.fileRef && props.fileUrl ? props.fileUrl(props.fileRef) : null));
46
+
47
+ const name = computed(() => exposed.value?.name || labelOfWire(props.path, '') || props.path || props.fileRef || '');
48
+ const kind = computed<'image' | 'video' | 'audio' | 'other'>(() => {
49
+ const m = mime.value;
50
+ if (m.startsWith('image/')) return 'image';
51
+ if (m.startsWith('video/')) return 'video';
52
+ if (m.startsWith('audio/')) return 'audio';
53
+ return 'other';
54
+ });
55
+ const tile = computed(() => fileIconTile({ type: 'file', basename: name.value }));
56
+
57
+ function release() {
58
+ // Only a blob URL this node minted is ours to revoke; an exposed copy's
59
+ // URL is the server's.
60
+ if (url.value && url.value.startsWith('blob:')) URL.revokeObjectURL(url.value);
61
+ url.value = null;
62
+ }
63
+
64
+ async function load() {
65
+ const mine = ++seq;
66
+ release();
67
+ mime.value = '';
68
+ if (props.fileRef) {
69
+ if (!exposed.value) {
70
+ state.value = 'error';
71
+ return;
72
+ }
73
+ url.value = exposed.value.url;
74
+ mime.value = exposed.value.mime || '';
75
+ state.value = 'ok';
76
+ return;
77
+ }
78
+ if (!props.api || !props.path) {
79
+ state.value = 'error';
80
+ return;
81
+ }
82
+ state.value = 'loading';
83
+ try {
84
+ const r = await props.api.fetchBlob(props.path);
85
+ if (mine !== seq) {
86
+ URL.revokeObjectURL(r.url);
87
+ return;
88
+ }
89
+ url.value = r.url;
90
+ mime.value = r.mime || '';
91
+ state.value = 'ok';
92
+ } catch {
93
+ if (mine !== seq) return;
94
+ state.value = 'error';
95
+ }
96
+ }
97
+
98
+ watch([() => props.path, () => props.fileRef], () => void load(), { immediate: true });
99
+ onBeforeUnmount(() => {
100
+ seq++;
101
+ release();
102
+ });
103
+ </script>
104
+
105
+ <template>
106
+ <figure class="fe-spreview" data-testid="surface-preview" :data-state="state">
107
+ <p v-if="state === 'loading'" class="fe-surface__text fe-surface__text--muted">{{ t('plugin.view.loading') }}</p>
108
+ <p v-else-if="state === 'error'" class="fe-surface__text fe-surface__text--muted">{{ t('plugin.view.preview_failed') }}</p>
109
+ <template v-else>
110
+ <img v-if="kind === 'image' && url" class="fe-spreview__media" :src="url" :alt="name" />
111
+ <video v-else-if="kind === 'video' && url" class="fe-spreview__media" :src="url" controls preload="metadata"></video>
112
+ <audio v-else-if="kind === 'audio' && url" class="fe-spreview__audio" :src="url" controls preload="metadata"></audio>
113
+ <embed v-else-if="fileRef && url" class="fe-spreview__media fe-spreview__embed" :src="url" :type="mime || undefined" />
114
+ <!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
115
+ <span v-else class="fe-spreview__tile" aria-hidden="true" v-html="tile"></span>
116
+ </template>
117
+ <figcaption class="fe-spreview__name" :title="path">{{ name }}</figcaption>
118
+ </figure>
119
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SurfaceProgress — a plugin surface's `progress` node. `value` 0..100, or
4
+ * null for "busy, no idea how long" (drawn as a moving bar).
5
+ */
6
+ import { computed } from 'vue';
7
+ import type { LocaleCode } from '../../../types/ExplorerConfig';
8
+ import type { PluginText } from '../../../types/Plugins';
9
+ import { useLocale } from '../../../composables/useLocale';
10
+ import { appTextOr } from '../../../lib/pluginLabel';
11
+
12
+ const props = defineProps<{
13
+ value: number | null | undefined;
14
+ label?: PluginText | string;
15
+ locale: LocaleCode;
16
+ }>();
17
+
18
+ const { t } = useLocale(() => props.locale);
19
+
20
+ const pct = computed<number | null>(() => {
21
+ const v = props.value;
22
+ if (v === null || v === undefined || !Number.isFinite(Number(v))) return null;
23
+ return Math.min(100, Math.max(0, Math.round(Number(v))));
24
+ });
25
+
26
+ /* ⚠ See `appTextOr`: the app's line for this reader, then filex's own. */
27
+ const text = computed(() => appTextOr(props.label, props.locale, () => t('plugin.view.busy')));
28
+ </script>
29
+
30
+ <template>
31
+ <div class="fe-sprogress" data-testid="surface-progress">
32
+ <div class="fe-sprogress__head">
33
+ <span class="fe-sprogress__label">{{ text }}</span>
34
+ <span v-if="pct !== null" class="fe-sprogress__pct">{{ pct }}%</span>
35
+ </div>
36
+ <div
37
+ class="fe-sprogress__track"
38
+ :class="{ 'is-indeterminate': pct === null }"
39
+ role="progressbar"
40
+ :aria-label="text"
41
+ aria-valuemin="0"
42
+ aria-valuemax="100"
43
+ :aria-valuenow="pct === null ? undefined : pct"
44
+ >
45
+ <div class="fe-sprogress__bar" :style="pct === null ? undefined : { width: `${pct}%` }"></div>
46
+ </div>
47
+ </div>
48
+ </template>