@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
@@ -6,10 +6,12 @@
6
6
  * must be identical in both, otherwise split view shows mismatched rows
7
7
  * (the trash row missing on one side → visible row-offset).
8
8
  */
9
+ import type { TagKind } from './tags';
9
10
  import { ref } from 'vue';
10
11
 
11
12
  import type { FileNode } from '../types/FileNode';
12
13
  import { E2E_MARKER_NAME } from './e2ecrypto';
14
+ import { isInternalName, isInternalPath } from './internalPaths';
13
15
 
14
16
  /** Adapter-strip: `s3-test://fileman/x` → `fileman/x`. */
15
17
  export function stripAdapter(p: string): string {
@@ -39,24 +41,21 @@ export function parentDirOf(path: string): string {
39
41
  }
40
42
 
41
43
  /**
42
- * Hide system/internal entries the user must never see as files:
43
- * thumbnails, version history, the soft-delete store, keepdir markers,
44
- * the desktop app's open-with scratch area and the E2E marker. Shared by
45
- * both panes' listing filters.
44
+ * Hide system/internal entries the user must never see as files: filex's own
45
+ * directories and the keep marker (lib/internalPaths — the one client list,
46
+ * held equal to the server's by a test), the virtual `.trash` row, and the E2E
47
+ * marker. Shared by both panes' listing filters.
48
+ *
49
+ * ⚠ Absolute, not a "hidden files" preference: a user who has switched hidden
50
+ * files on is asking to see their OWN dotfiles, not ours. The server already
51
+ * leaves these out of every listing; this is the seatbelt for an embed talking
52
+ * to an older server — which is also why it judges the whole PATH, so a search
53
+ * hit such a server returns from inside `.filex-open` is dropped too.
46
54
  */
47
55
  export function filterInternalEntries(files: FileNode[]): FileNode[] {
48
56
  return (files || []).filter((f) => {
49
- if (f.path.includes('.thumbs')) return false;
50
- if (f.path.includes('.versions') || f.basename === '.versions') return false;
57
+ if (isInternalName(f.basename) || isInternalPath(f.path)) return false;
51
58
  if (f.basename === '.trash') return false;
52
- // The desktop app's "open this document with filex" scratch area. Real
53
- // files live in it for the length of one editing session, but they are the
54
- // app's plumbing, not the user's documents -- and a user who has switched
55
- // hidden files on is asking to see their OWN dotfiles, not ours. Hidden
56
- // absolutely, like .filex-trash and .versions; stale copies are swept by
57
- // the desktop app itself, not by hand.
58
- if (f.basename === '.filex-open') return false;
59
- if (f.basename === '.keepdir') return false;
60
59
  if (f.basename === E2E_MARKER_NAME) return false;
61
60
  return true;
62
61
  });
@@ -272,16 +271,50 @@ export const VIRTUAL_SEGMENTS: Record<string, string> = {
272
271
  */
273
272
  export const TAG_SEGMENT_PREFIX = '.tag~';
274
273
 
275
- /** `invoices` → `.tag~invoices`. */
276
- export function makeTagSegment(tag: string): string {
277
- return `${TAG_SEGMENT_PREFIX}${tag}`;
274
+ /**
275
+ * etiket:k2 (v0.43.0) — the KIND of a tag view. A person can have a personal
276
+ * "rapor" and their team a "rapor"; the navigation panel lists them apart, so
277
+ * each opens its own view: `.mytag~rapor` (personal), `.teamtag~rapor` (team).
278
+ * The plain `.tag~rapor` stays and means BOTH — every link and restored tab
279
+ * from before kinds existed keeps opening what it opened then.
280
+ *
281
+ * ⚠ Listed longest-first is not needed (no prefix is a prefix of another),
282
+ * but every reader goes through `tagViewOfSegment`, never a `startsWith` of
283
+ * its own, so a fourth kind would be one line here.
284
+ */
285
+ const TAG_KIND_PREFIXES: ReadonlyArray<readonly [string, TagKind | '']> = [
286
+ ['.mytag~', 'personal'],
287
+ ['.teamtag~', 'team'],
288
+ [TAG_SEGMENT_PREFIX, ''],
289
+ ];
290
+
291
+ /** `invoices` → `.tag~invoices`; with a kind, `.mytag~` / `.teamtag~`. */
292
+ export function makeTagSegment(tag: string, kind: TagKind | '' = ''): string {
293
+ const prefix = TAG_KIND_PREFIXES.find(([, k]) => k === kind)?.[0] ?? TAG_SEGMENT_PREFIX;
294
+ return `${prefix}${tag}`;
295
+ }
296
+
297
+ /** A tag-view segment's name and kind ('' = both), or null for anything else
298
+ * (a bare `.tag~` included). */
299
+ export function tagViewOfSegment(segment: string): { tag: string; kind: TagKind | '' } | null {
300
+ for (const [prefix, kind] of TAG_KIND_PREFIXES) {
301
+ if (segment.startsWith(prefix)) {
302
+ const tag = segment.slice(prefix.length);
303
+ return tag ? { tag, kind } : null;
304
+ }
305
+ }
306
+ return null;
278
307
  }
279
308
 
280
- /** `.tag~invoices` → `invoices`; '' for anything else (incl. a bare `.tag~`). */
309
+ /** `.tag~invoices` (or `.mytag~` / `.teamtag~`) → `invoices`; '' otherwise. */
281
310
  export function tagOfSegment(segment: string): string {
282
- return segment.startsWith(TAG_SEGMENT_PREFIX)
283
- ? segment.slice(TAG_SEGMENT_PREFIX.length)
284
- : '';
311
+ return tagViewOfSegment(segment)?.tag ?? '';
312
+ }
313
+
314
+ /** The kind of a tag-view path: 'personal', 'team', or '' (both / not a tag view). */
315
+ export function tagKindOfPath(path: string): TagKind | '' {
316
+ const clean = (path || '').replace(/^\/+|\/+$/g, '');
317
+ return tagViewOfSegment(clean.split('/').pop() || clean)?.kind ?? '';
285
318
  }
286
319
 
287
320
  /** True when `path` (user-facing form) IS a tag listing. */
@@ -308,8 +341,10 @@ export function virtualSegmentKey(segment: string): string {
308
341
  export function virtualSegmentLabel(segment: string, t: (key: string) => string): string {
309
342
  const key = VIRTUAL_SEGMENTS[segment];
310
343
  if (key) return t(key);
311
- const tag = tagOfSegment(segment);
312
- return tag ? `#${tag}` : '';
344
+ const view = tagViewOfSegment(segment);
345
+ if (!view) return '';
346
+ // etiket:k2 — the crumb says whose tag this is: "#rapor · Kişisel".
347
+ return view.kind ? `#${view.tag} · ${t(`tags.kind.${view.kind}`)}` : `#${view.tag}`;
313
348
  }
314
349
 
315
350
  /**
@@ -0,0 +1,26 @@
1
+ /**
2
+ * modalStack — which explorer dialog is on top.
3
+ *
4
+ * Every open `modals/Modal.vue` registers here, and only the LAST one
5
+ * answers the keyboard (Escape, the Tab trap). Two dialogs open at once is
6
+ * ordinary — a confirmation over a plugin's screen, the destination picker
7
+ * over a move — and Escape must close the one in front, not both.
8
+ *
9
+ * ⚠ Module state on purpose: `<script setup>` top-level declarations are
10
+ * per INSTANCE, so a stack declared inside the component would hold one
11
+ * entry per dialog and every dialog would think it was on top.
12
+ */
13
+ const stack: symbol[] = [];
14
+
15
+ export function pushModal(id: symbol): void {
16
+ stack.push(id);
17
+ }
18
+
19
+ export function popModal(id: symbol): void {
20
+ const i = stack.lastIndexOf(id);
21
+ if (i >= 0) stack.splice(i, 1);
22
+ }
23
+
24
+ export function isTopModal(id: symbol): boolean {
25
+ return stack[stack.length - 1] === id;
26
+ }
@@ -52,6 +52,15 @@ export function nodeRowToFileNode(
52
52
  if (ctx.multiStorageRoot && !storageName) return null;
53
53
  const name = String(row.name ?? rel.split('/').pop() ?? '');
54
54
  const isDir = row.type === 'dir';
55
+ /* issue #34 — these endpoints answer with the raw node row, so `type` here
56
+ is `model.NodeType`: 'file', 'dir' OR 'symlink'. The listing projector
57
+ collapses that third value to 'file' and sets a `symlink` flag beside it;
58
+ `handlers/meta.go` does not, because it ships the node struct as it is.
59
+ Without this line a link the server will not follow appears on Recent,
60
+ Starred, a tag view or the Home tray as a plain file with no badge and no
61
+ explanation — the same fix reaching the folder listing and stopping at
62
+ the view beside it, which is the split lesson #174 was written about. */
63
+ const isSymlink = row.type === 'symlink';
55
64
  const size = typeof row.size === 'number' ? row.size : 0;
56
65
  const id = typeof row.id === 'number' ? row.id : undefined;
57
66
  /* ⚠⚠ THE CONTEXT MENU MUST BE THE SAME EVERYWHERE (owner, 2026-09-19). The
@@ -98,9 +107,15 @@ export function nodeRowToFileNode(
98
107
  mime_type: typeof row.mime === 'string' ? row.mime : '',
99
108
  ...(perm ? { perm } : {}),
100
109
  read_only: readOnly,
101
- // Keyed by node id. A file with no rendered thumbnail 404s here and the
102
- // view falls back to its icon — the contract the ordinary listing has too.
103
- thumb_url: !isDir && id !== undefined ? `/api/files/thumb/${id}` : undefined,
110
+ ...(isSymlink ? { symlink: true } : {}),
111
+ // ⚠⚠ The SERVER's `thumb_url`, never one built here. It used to be
112
+ // `/api/files/thumb/<id>` for every file, and the endpoint answers
113
+ // `404 "not ready"` for everything it has not rendered — a docx, a
114
+ // markdown note, a diagram: opening Recent with ten such files fired ten
115
+ // requests and all ten 404'd (2026-09-21). The rows now carry the URL
116
+ // exactly when there is an image behind it (handlers/meta.go, the folder
117
+ // listing's rule), so a file with none is never asked about.
118
+ thumb_url: !isDir && typeof row.thumb_url === 'string' && row.thumb_url ? row.thumb_url : undefined,
104
119
  extra_metadata: {},
105
120
  } as unknown as FileNode;
106
121
  }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * pdfFieldRules — what a `text` field on a PDF will accept, as pure data.
3
+ *
4
+ * A signing app does not only want "a box for some words": it wants a tax
5
+ * number that is digits, a date in the shape the paper form prints, an
6
+ * address that fits the line. So a `text` field may carry a `rule`, and the
7
+ * browser enforces it WHILE the person types rather than after they submit —
8
+ * an invalid signature page that only says so on the way out is a page that
9
+ * gets signed twice.
10
+ *
11
+ * ⚠ The rule travels back to the plugin with the value
12
+ * (`{fields: [{id, value, font?, rule?}]}`, docs/APP-PLUGINS-API.md →
13
+ * `pdf-fields` props) so the stamper formats what it is handed instead of
14
+ * guessing from the string: `01/02/2026` is a date to a rule and ambiguous
15
+ * text to a parser.
16
+ *
17
+ * ⚠ This is a CONVENIENCE, not a security boundary. The browser is the only
18
+ * thing that runs it; a plugin that cares re-checks what it is given. Same
19
+ * relationship `applies` has with the server's re-check.
20
+ */
21
+
22
+ /**
23
+ * What a `text` field accepts. `any` is the default and means "anything".
24
+ *
25
+ * ⚠⚠ v3 §3.3 — there is no `date` rule any more. `date` is a FIELD TYPE,
26
+ * and two ways to ask for the same thing is how you get two answers: a date
27
+ * placed as a date field arrived as `YYYY-MM-DD` and a date typed into a text
28
+ * field under the old rule arrived as `DD/MM/YYYY`, so the stamping plugin
29
+ * had to guess which of its own boxes it was looking at. An older manifest's
30
+ * `kind: "date"` is read as `any` rather than refused — the box keeps taking
31
+ * what the person types, it just stops pretending to be a second date
32
+ * control.
33
+ */
34
+ export type PdfRuleKind = 'any' | 'number' | 'email';
35
+
36
+ export const PDF_RULE_KINDS: readonly PdfRuleKind[] = ['any', 'number', 'email'];
37
+
38
+ export interface PdfFieldRule {
39
+ kind: PdfRuleKind;
40
+ /** Shortest accepted length in characters (0 = no floor). */
41
+ min?: number;
42
+ /** Longest accepted length in characters (0 = no ceiling). */
43
+ max?: number;
44
+ }
45
+
46
+ export function isRuleKind(v: unknown): v is PdfRuleKind {
47
+ return typeof v === 'string' && PDF_RULE_KINDS.includes(v as PdfRuleKind);
48
+ }
49
+
50
+ /** The wire's `rule`, reduced to something drawable; `null` when there is none. */
51
+ export function normalizeRule(raw: unknown): PdfFieldRule | null {
52
+ if (!raw || typeof raw !== 'object') return null;
53
+ const r = raw as Record<string, unknown>;
54
+ const kind: PdfRuleKind = isRuleKind(r.kind) ? r.kind : 'any';
55
+ const out: PdfFieldRule = { kind };
56
+ const min = Math.trunc(Number(r.min));
57
+ const max = Math.trunc(Number(r.max));
58
+ if (Number.isFinite(min) && min > 0) out.min = min;
59
+ if (Number.isFinite(max) && max > 0) out.max = max;
60
+ // A bare `{kind: "any"}` with no bounds says nothing; treat it as no rule so
61
+ // it does not travel back as noise.
62
+ if (out.kind === 'any' && out.min === undefined && out.max === undefined) return null;
63
+ return out;
64
+ }
65
+
66
+ /** `inputmode` for the on-screen keyboard a rule wants. */
67
+ export function ruleInputMode(rule: PdfFieldRule | null): 'text' | 'numeric' | 'email' {
68
+ if (!rule) return 'text';
69
+ if (rule.kind === 'number') return 'numeric';
70
+ if (rule.kind === 'email') return 'email';
71
+ return 'text';
72
+ }
73
+
74
+ /** The longest string the rule allows to be typed at all. */
75
+ export function ruleMaxLength(rule: PdfFieldRule | null): number | undefined {
76
+ if (!rule) return undefined;
77
+ return rule.max && rule.max > 0 ? rule.max : undefined;
78
+ }
79
+
80
+ /**
81
+ * Type-as-you-go shaping. Returns what the field should hold after this
82
+ * keystroke — never an error: a rule refuses a CHARACTER here, and says why
83
+ * only in `ruleError` once there is something to judge.
84
+ */
85
+ export function applyRule(raw: string, rule: PdfFieldRule | null): string {
86
+ let v = String(raw ?? '');
87
+ if (!rule) return v;
88
+ if (rule.kind === 'number') {
89
+ // Digits, one leading minus, one decimal separator — the separator kept
90
+ // as typed so a Turkish keyboard's comma is not silently rewritten.
91
+ const neg = v.startsWith('-');
92
+ let seen = false;
93
+ v = v
94
+ .split('')
95
+ .filter((ch) => {
96
+ if (ch >= '0' && ch <= '9') return true;
97
+ if ((ch === '.' || ch === ',') && !seen) {
98
+ seen = true;
99
+ return true;
100
+ }
101
+ return false;
102
+ })
103
+ .join('');
104
+ if (neg) v = `-${v}`;
105
+ } else if (rule.kind === 'email') {
106
+ v = v.replace(/\s+/g, '');
107
+ }
108
+ const max = ruleMaxLength(rule);
109
+ if (max && v.length > max) v = v.slice(0, max);
110
+ return v;
111
+ }
112
+
113
+ /** Codes `ruleErrorText` turns into words; `''` when the value is acceptable. */
114
+ export type PdfRuleError = '' | 'number' | 'email' | 'min' | 'max';
115
+
116
+ /** Is `value` acceptable under `rule`? An empty value is never wrong here — `required` is a separate question. */
117
+ export function ruleError(value: unknown, rule: PdfFieldRule | null): PdfRuleError {
118
+ const v = typeof value === 'string' ? value.trim() : '';
119
+ if (!rule || v === '') return '';
120
+ if (rule.min && v.length < rule.min) return 'min';
121
+ if (rule.max && v.length > rule.max) return 'max';
122
+ if (rule.kind === 'number') return /^-?\d+([.,]\d+)?$/.test(v) ? '' : 'number';
123
+ if (rule.kind === 'email') return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v) ? '' : 'email';
124
+ return '';
125
+ }
126
+