@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,104 @@
1
+ /**
2
+ * serviceGate — what an action that needs an OPTIONAL external service
3
+ * (ONLYOFFICE, draw.io, the converter…) becomes when that service is missing.
4
+ *
5
+ * ⚠⚠ The owner's rule, 2026-09-21, after `Config fetch 503: {"error":
6
+ * "onlyoffice not configured"}` was shown to a person who had clicked "Open" on
7
+ * a .docx on an install with no document server:
8
+ *
9
+ * "disabled with a reason for administrators, hidden for everybody else"
10
+ *
11
+ * An administrator CAN fix it, so a greyed entry that says what is missing and
12
+ * where to set it up is useful to them. For everybody else it is a button that
13
+ * can never work and only invites a click, so it is not offered at all. And
14
+ * nobody, ever, is shown a raw status or a JSON body.
15
+ *
16
+ * ⚠ One function, so every entry that depends on a service answers the same
17
+ * way (filex lesson #67: a rule written twice drifts the first time one copy is
18
+ * touched). "Is this caller an administrator" is the server's answer
19
+ * (`capabilities.caller_admin` — the same checks the admin routes apply,
20
+ * supertenant included), never a guess made in the browser.
21
+ */
22
+
23
+ /** The fields a `ContextAction` takes from the gate. */
24
+ export interface ServiceGate {
25
+ hidden?: boolean;
26
+ disabled?: boolean;
27
+ title?: string;
28
+ }
29
+
30
+ /**
31
+ * `available` — the service is configured and healthy.
32
+ * `callerAdmin` — this caller could go and configure it.
33
+ * `reason` — the sentence an administrator reads (what is missing, where to
34
+ * fix it), already translated.
35
+ */
36
+ export function gateOnService(available: boolean, callerAdmin: boolean, reason: string): ServiceGate {
37
+ if (available) return {};
38
+ if (callerAdmin) return { disabled: true, title: reason };
39
+ return { hidden: true };
40
+ }
41
+
42
+ /**
43
+ * The extensions the document server opens. ⚠ The ONE list: the preview
44
+ * modal decides "this is an office document" from it and the explorer's menu
45
+ * decides "Open needs ONLYOFFICE" from it — two lists would let a .rtf be
46
+ * previewed as office and still offered an Open that cannot work.
47
+ */
48
+ /**
49
+ * THE LEGACY CONVERTER — the iframe service behind `FILEX_CONVERT_URL` /
50
+ * External services → Converter — against the Convert APP.
51
+ *
52
+ * ⚠⚠ ONE of everything. With both in place an administrator's menu on a .docx
53
+ * read "Dönüştür" (the service, greyed, "not set up") right above
54
+ * "Dönüştür…" (the app): two entries with one name and two behaviours (QA,
55
+ * 2026-09-21). The release plan's rule, made unbreakable by construction:
56
+ *
57
+ * - the Convert app is offered on this instance → the legacy entry is never
58
+ * offered, to anybody, on any file (not "on files the app does not take":
59
+ * the two would still meet on the next file);
60
+ * - the legacy service is not configured → not offered either, not even
61
+ * greyed for an administrator: its replacement is the app, and pointing
62
+ * at a retiring service to "set it up" sends them the wrong way;
63
+ * - configured but not answering → the usual split (gateOnService):
64
+ * greyed with the reason for an administrator, hidden for everybody else;
65
+ * - configured and answering → offered; an administrator reads that it is
66
+ * being retired and what replaces it.
67
+ */
68
+ export interface LegacyConvertInput {
69
+ /** The Convert app has an action on this instance (lib/pluginMenu `convertAppOffered`). */
70
+ appOffered: boolean;
71
+ /** The legacy service is switched on (or the host passed its own URL). */
72
+ configured: boolean;
73
+ /** …and it answers its health check. */
74
+ healthy: boolean;
75
+ callerAdmin: boolean;
76
+ /** Why it is greyed: configured but not answering. */
77
+ unhealthyReason: string;
78
+ /** What an administrator reads on a working entry: it is being retired. */
79
+ adminNote: string;
80
+ }
81
+
82
+ export function legacyConvertGate(o: LegacyConvertInput): ServiceGate {
83
+ if (o.appOffered || !o.configured) return { hidden: true };
84
+ const g = gateOnService(o.healthy, o.callerAdmin, o.unhealthyReason);
85
+ if (g.hidden || g.disabled) return g;
86
+ return o.callerAdmin ? { title: o.adminNote } : {};
87
+ }
88
+
89
+ export const OFFICE_EXTS: readonly string[] = [
90
+ 'docx',
91
+ 'doc',
92
+ 'xlsx',
93
+ 'xls',
94
+ 'pptx',
95
+ 'ppt',
96
+ 'odt',
97
+ 'ods',
98
+ 'odp',
99
+ 'rtf',
100
+ ];
101
+
102
+ export function isOfficeExt(ext: string | null | undefined): boolean {
103
+ return OFFICE_EXTS.includes(String(ext ?? '').toLowerCase());
104
+ }
@@ -9,7 +9,21 @@
9
9
  // from the same rule. A surface that clamps differently would mean the same
10
10
  // product behaves two ways.
11
11
 
12
- import { formatInstant } from '../composables/useLocale'; /* zaman:z1 */
12
+ import { formatWhen, translate } from '../composables/useLocale'; /* zaman:z1 */
13
+
14
+ /**
15
+ * A catalogue string for a bare locale code — this is a module, not a
16
+ * component, so it cannot call `useLocale`. ⚠ Through `translate`, which is
17
+ * `t()`'s own lookup: a language pack's words reach these lines too (they were
18
+ * `tr ? … : …` pairs, which a pack could not translate and which printed
19
+ * TURKISH under any language that was not English), and "{days} days" takes
20
+ * its form by the pack language's plural rule, not English's. It had a copy
21
+ * of `t()`'s lookup, and the copy read English's `_one` under a pack that had
22
+ * written only the plain form.
23
+ */
24
+ function say(locale: string, key: string, vars: Record<string, string | number> = {}): string {
25
+ return translate(locale, key, vars);
26
+ }
13
27
 
14
28
  export interface ExpiryOption {
15
29
  /** Days; 0 = never. */
@@ -76,11 +90,11 @@ export function expiryInputMax(maxDays: number | undefined, now: Date = new Date
76
90
  /** "Valid until 30 Aug 2026, 14:05" / "Does not expire" from the server's `expires_at`. */
77
91
  export function validUntilLine(
78
92
  expiresAt: string | null | undefined,
79
- locale: 'tr' | 'en',
93
+ locale: string,
80
94
  /** The explorer whose clock applies (lib/timezone EXPLORER_CLOCK). */
81
95
  clock?: symbol,
82
96
  ): string {
83
- if (!expiresAt) return locale === 'tr' ? 'Bu bağlantının süresi yoktur.' : 'This link does not expire.';
97
+ if (!expiresAt) return say(locale, 'share.ttl.never');
84
98
  const d = new Date(expiresAt);
85
99
  // zaman:z1 — the viewer's chosen clock, not the browser's. "Valid until
86
100
  // 14:05" is a deadline, and a deadline printed in somebody else's zone is
@@ -90,19 +104,19 @@ export function validUntilLine(
90
104
  // then map the tag itself, to 'en-GB', while useLocale mapped it to
91
105
  // 'en-US': the same deadline was spelled "20 Sept 2026, 13:53" in the share
92
106
  // dialog and "Sep 20, 2026, 1:53 PM" in the listing behind it.
93
- const when = Number.isNaN(d.getTime())
94
- ? expiresAt
95
- : formatInstant(d, locale, { dateStyle: 'medium', timeStyle: 'short' }, clock);
96
- return locale === 'tr' ? `Bu bağlantı ${when} tarihine kadar geçerli.` : `This link is valid until ${when}.`;
107
+ //
108
+ // ⚠ And the FORMAT is the explorer's (`formatWhen`), not a `dateStyle:
109
+ // 'medium'` pair of its own: that printed "21 Eyl 2026 15:02" beside the
110
+ // listing's "21 Eyl 2026, 14:50" (QA, 2026-09-21).
111
+ const when = Number.isNaN(d.getTime()) ? expiresAt : formatWhen(d, locale, { time: true }, clock);
112
+ return say(locale, 'share.ttl.until', { when });
97
113
  }
98
114
 
99
115
  /** The hint under an expiry control: what the server will allow at most. */
100
- export function ttlCeilingHint(maxDays: number | undefined, locale: 'tr' | 'en'): string {
116
+ export function ttlCeilingHint(maxDays: number | undefined, locale: string): string {
101
117
  const max = maxDays && maxDays > 0 ? Math.floor(maxDays) : 0;
102
118
  if (!max) return '';
103
- return locale === 'tr'
104
- ? `Bağlantılar en fazla ${max} gün geçerli olabilir (sunucu ayarı).`
105
- : `Links can be valid for at most ${max} day${max === 1 ? '' : 's'} (server setting).`;
119
+ return say(locale, 'share.ttl.ceiling', { days: max });
106
120
  }
107
121
 
108
122
  /**
@@ -0,0 +1,85 @@
1
+ /**
2
+ * signFonts — the closed set of faces a signature may be written in.
3
+ *
4
+ * A typed signature and a `pdf-fields` text field both let the signer choose
5
+ * how their words are drawn, and the choice TRAVELS: it rides back to the
6
+ * plugin as `font` (see docs/APP-PLUGINS-API.md → `pdf-fields` props), which
7
+ * then stamps the PDF in that face. So the browser and the stamping plugin
8
+ * have to agree on a small, stable vocabulary — five keys, not a free-text
9
+ * CSS family — and this file is that vocabulary.
10
+ *
11
+ * ⚠ ONE definition, three readers: the signature pad's "type" mode, the
12
+ * pdf-fields overlay and anything that later re-renders a stored value. A
13
+ * second copy is how a signature typed in Caveat comes back in Inter.
14
+ *
15
+ * ⚠ The faces themselves are self-hosted (`styles/sign-fonts.css` +
16
+ * `assets/fonts/`), never a CDN: see that stylesheet's note for why a signing
17
+ * screen in particular must not phone out for a font.
18
+ */
19
+
20
+ /** The wire value of a `font` choice. */
21
+ export type SignFontKey = 'caveat' | 'dancing-script' | 'homemade-apple' | 'inter' | 'source-serif';
22
+
23
+ export interface SignFont {
24
+ key: SignFontKey;
25
+ /** What the person sees in the picker — a font's name is not translated. */
26
+ name: string;
27
+ /** Handwriting faces are offered first; the formal pair closes the list. */
28
+ kind: 'hand' | 'formal';
29
+ /** The class `styles/sign-fonts.css` declares for this key. */
30
+ className: string;
31
+ /**
32
+ * The same stack as a string, for a canvas `ctx.font` (canvas takes no
33
+ * class). ⚠ `inter` resolves through `var(--fe-font)` in CSS but a canvas
34
+ * cannot read a custom property, so its literal stack is spelled out here
35
+ * and kept in step with `styles/variables.css`.
36
+ */
37
+ stack: string;
38
+ }
39
+
40
+ /** Handwriting first, formal last — the order the picker draws. */
41
+ export const SIGN_FONTS: readonly SignFont[] = [
42
+ { key: 'caveat', name: 'Caveat', kind: 'hand', className: 'fe-signfont--caveat', stack: "'Caveat', cursive" },
43
+ {
44
+ key: 'dancing-script',
45
+ name: 'Dancing Script',
46
+ kind: 'hand',
47
+ className: 'fe-signfont--dancing-script',
48
+ stack: "'Dancing Script', cursive",
49
+ },
50
+ {
51
+ key: 'homemade-apple',
52
+ name: 'Homemade Apple',
53
+ kind: 'hand',
54
+ className: 'fe-signfont--homemade-apple',
55
+ stack: "'Homemade Apple', cursive",
56
+ },
57
+ {
58
+ key: 'inter',
59
+ name: 'Inter',
60
+ kind: 'formal',
61
+ className: 'fe-signfont--inter',
62
+ stack: 'Inter, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
63
+ },
64
+ {
65
+ key: 'source-serif',
66
+ name: 'Source Serif 4',
67
+ kind: 'formal',
68
+ className: 'fe-signfont--source-serif',
69
+ stack: "'Source Serif 4', Georgia, 'Times New Roman', serif",
70
+ },
71
+ ];
72
+
73
+ /** The face a screen starts on when the plugin named none. */
74
+ export const DEFAULT_SIGN_FONT: SignFontKey = 'caveat';
75
+
76
+ /** The named face, or the default — never `undefined`, so no caller branches. */
77
+ export function signFont(key: unknown): SignFont {
78
+ const k = String(key ?? '');
79
+ return SIGN_FONTS.find((f) => f.key === k) ?? SIGN_FONTS.find((f) => f.key === DEFAULT_SIGN_FONT)!;
80
+ }
81
+
82
+ /** Is this one of the five? (A value off the wire is not trusted.) */
83
+ export function isSignFontKey(v: unknown): v is SignFontKey {
84
+ return typeof v === 'string' && SIGN_FONTS.some((f) => f.key === v);
85
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * signaturePad — the `signature-pad` node's rules without a canvas.
3
+ *
4
+ * Which modes a surface offers, how big an uploaded picture may be and how
5
+ * it is fitted, what the value on the wire looks like. The component draws;
6
+ * this decides, so the decisions can be tested on their own.
7
+ */
8
+ import { DEFAULT_SIGN_FONT, isSignFontKey, type SignFontKey } from './signFonts';
9
+
10
+ export const SIGNATURE_MODES = ['draw', 'type', 'upload'] as const;
11
+ export type SignatureMode = (typeof SIGNATURE_MODES)[number];
12
+
13
+ /** `data.values[id]` of a signed pad. */
14
+ export interface SignatureValue {
15
+ png_b64: string;
16
+ mode: SignatureMode;
17
+ /**
18
+ * `type` mode only: which of the five faces the signer picked
19
+ * (`lib/signFonts`).
20
+ *
21
+ * ⚠ It travels even though the PNG is already rendered in that face,
22
+ * because the plugin may re-render the name at print resolution rather
23
+ * than upscale a 600×200 bitmap — and it cannot ask the screen which
24
+ * font was used after the fact.
25
+ */
26
+ font?: SignFontKey;
27
+ }
28
+
29
+ /** An uploaded picture may weigh this much. */
30
+ export const SIGNATURE_UPLOAD_MAX_BYTES = 200 * 1024;
31
+ /** The box every signature is fitted into (CSS px). */
32
+ export const SIGNATURE_MAX_WIDTH = 600;
33
+ export const SIGNATURE_MAX_HEIGHT = 200;
34
+ /** The pad's own size when the node names none. */
35
+ export const SIGNATURE_DEFAULT_WIDTH = 480;
36
+ export const SIGNATURE_DEFAULT_HEIGHT = 160;
37
+
38
+ /** Ink is black whatever the theme: it ends up on paper, not on a screen. */
39
+ export const SIGNATURE_INK = '#111111';
40
+ /**
41
+ * The face a typed signature is drawn in when the signer has not chosen one.
42
+ *
43
+ * ⚠⚠ It is a KEY from `lib/signFonts`, not a CSS stack any more. The stack
44
+ * that used to live here named fonts the machine might happen to have
45
+ * ('Segoe Script', 'Apple Chancery', …), so the same name signed on a Mac,
46
+ * on Windows and on a phone came out in three different hands — and on a
47
+ * machine with none of them, in the browser's generic cursive. The five faces
48
+ * are shipped with the package now, so every signer gets the one they picked.
49
+ */
50
+ export const SIGNATURE_FONT_DEFAULT: SignFontKey = DEFAULT_SIGN_FONT;
51
+
52
+ export function isSignatureMode(v: unknown): v is SignatureMode {
53
+ return typeof v === 'string' && (SIGNATURE_MODES as readonly string[]).includes(v);
54
+ }
55
+
56
+ /** The modes the node offers, in the catalogue's order; every mode when it names none. */
57
+ export function signatureModes(raw: unknown): SignatureMode[] {
58
+ if (!Array.isArray(raw)) return [...SIGNATURE_MODES];
59
+ const picked = raw.filter(isSignatureMode);
60
+ return picked.length ? SIGNATURE_MODES.filter((m) => picked.includes(m)) : [...SIGNATURE_MODES];
61
+ }
62
+
63
+ export function isSignatureValue(v: unknown): v is SignatureValue {
64
+ const o = v as SignatureValue | null;
65
+ return !!o && typeof o === 'object' && typeof o.png_b64 === 'string' && o.png_b64 !== '' && isSignatureMode(o.mode);
66
+ }
67
+
68
+ /** The face a value was typed in, or the default. Never `undefined`. */
69
+ export function signatureFontOf(v: unknown): SignFontKey {
70
+ const o = v as SignatureValue | null;
71
+ return o && isSignFontKey(o.font) ? o.font : SIGNATURE_FONT_DEFAULT;
72
+ }
73
+
74
+ /** The pad's drawing size: the node's numbers, bounded, else the defaults. */
75
+ export function padSize(width: unknown, height: unknown): { width: number; height: number } {
76
+ const w = Number(width);
77
+ const h = Number(height);
78
+ return {
79
+ width: Number.isFinite(w) && w > 0 ? Math.min(SIGNATURE_MAX_WIDTH, Math.max(120, Math.round(w))) : SIGNATURE_DEFAULT_WIDTH,
80
+ height: Number.isFinite(h) && h > 0 ? Math.min(SIGNATURE_MAX_HEIGHT, Math.max(60, Math.round(h))) : SIGNATURE_DEFAULT_HEIGHT,
81
+ };
82
+ }
83
+
84
+ /** `w×h` scaled down (never up) to fit inside `maxW×maxH`, aspect kept. */
85
+ export function fitWithin(w: number, h: number, maxW: number, maxH: number): { width: number; height: number } {
86
+ if (!(w > 0) || !(h > 0)) return { width: maxW, height: maxH };
87
+ const k = Math.min(1, maxW / w, maxH / h);
88
+ return { width: Math.max(1, Math.round(w * k)), height: Math.max(1, Math.round(h * k)) };
89
+ }
90
+
91
+ /** `data:image/png;base64,AAAA` → `AAAA`. Anything without the prefix is returned as is. */
92
+ export function stripDataUrl(s: string): string {
93
+ const i = s.indexOf('base64,');
94
+ return i >= 0 ? s.slice(i + 'base64,'.length) : s;
95
+ }
96
+
97
+ export type UploadVerdict = 'ok' | 'too_big' | 'bad_type';
98
+
99
+ /** PNG or JPEG, at most the ceiling. */
100
+ export function uploadVerdict(file: { type: string; size: number }): UploadVerdict {
101
+ if (file.type !== 'image/png' && file.type !== 'image/jpeg') return 'bad_type';
102
+ if (file.size > SIGNATURE_UPLOAD_MAX_BYTES) return 'too_big';
103
+ return 'ok';
104
+ }
105
+
106
+ /** The largest font size (px) at which `text` fits `maxWidth`, measured by the caller. */
107
+ export function fitFontSize(measure: (px: number) => number, maxWidth: number, maxHeight: number): number {
108
+ let px = Math.min(96, Math.floor(maxHeight * 0.6));
109
+ while (px > 12 && measure(px) > maxWidth) px -= 2;
110
+ return px;
111
+ }
@@ -26,8 +26,11 @@
26
26
  * · the RULES (`byActiveKey`, `compareNodes`, folders-first, the two click
27
27
  * vocabularies) — written once, closed over each store;
28
28
  * · the LOCALE the `type` key sorts in — one alphabet for the whole bundle;
29
- * · the persisted GLOBAL DEFAULT (`filex.list-sort`) — "the last sort a
30
- * person chose", which is one fact no matter which pane they chose it in.
29
+ * · the DEFAULT a folder nobody configured opens with — the person's, else
30
+ * the instance's (`lib/viewPrefs`). ⚠ Since v0.43 it is NOT "the last sort
31
+ * a person chose": a choice belongs to the folder it was made in, and
32
+ * nothing in this file writes the default (see the note where `persist`
33
+ * used to be).
31
34
  *
32
35
  * ⚠ Panes find their store by INJECTION (`provideSortStore` / `useSortStore`),
33
36
  * not by a prop threaded through every view: `ListView` and `FilterBar` are
@@ -61,6 +64,10 @@ import type { LocaleCode } from '../types/ExplorerConfig';
61
64
  import { messages } from '../locales';
62
65
  import { typeLabelKey } from './fileIcons';
63
66
  import { byFoldersFirst } from './listing';
67
+ /* ⚠ The DEFAULT a folder nobody configured opens with (the person's, else the
68
+ * instance's). `viewPrefs` imports only TYPES from this file, so there is no
69
+ * runtime cycle between the two. */
70
+ import { defaultFolderView, onViewPrefsApplied, viewPrefsReady } from './viewPrefs';
64
71
 
65
72
  export type SortKey = 'name' | 'type' | 'modified' | 'size';
66
73
  export type SortDir = 'asc' | 'desc';
@@ -247,48 +254,71 @@ export interface SortStore {
247
254
  sortListing(files: FileNode[], order?: ListingOrder): FileNode[];
248
255
  }
249
256
 
250
- function persist(key: SortKey, dir: SortDir): void {
251
- try {
252
- localStorage.setItem(SORT_LS_KEY, JSON.stringify({ key, dir }));
253
- } catch {
254
- /* quota / private mode — the in-memory refs still moved */
255
- }
256
- }
257
+ /*
258
+ * A person CHOSE a sort — and that is ALL it is. There is no `persist` here
259
+ * any more.
260
+ *
261
+ * ⚠⚠ This used to write the choice as the GLOBAL DEFAULT (on the account and
262
+ * into `filex.list-sort`), under the rule "your last choice becomes the
263
+ * default for every folder you have never set up". That rule was the leak the
264
+ * owner reported on 2026-09-21 — "Explore içindeki değişikliklerimiz o klasör
265
+ * özelinde olmalı; tüm klasörlerde görünüm değişikliği geçerli oluyor": sort
266
+ * folder A by size and every untouched folder came up sorted by size. A
267
+ * choice now belongs to the folder it was made in, and the explorer records
268
+ * it there (FileExplorer → `rememberFolder`); the default is set on purpose,
269
+ * in the person's settings, and nothing in this file writes it.
270
+ *
271
+ * ⚠ `filex.list-sort` is still READ (`readStored`) — for the first paint only,
272
+ * as the cache of the resolved default that `lib/viewPrefs` keeps. Writing a
273
+ * click into it would paint the next page load in the last folder's sort
274
+ * before the document corrected it: the same leak, for half a second.
275
+ */
276
+
277
+ /**
278
+ * Every store this module has handed out.
279
+ *
280
+ * ⚠ Needed so a document arriving from the SERVER can reach the panes that
281
+ * are already on screen — the main one, the split one, an embed's. Without
282
+ * it the account's answer would only apply to panes created after it landed,
283
+ * which is "it works if you navigate first".
284
+ */
285
+ const liveStores = new Set<SortStore>();
257
286
 
258
287
  /**
259
- * A fresh sort store, seeded from the global default.
288
+ * The default each store was last put on — its creation seed, then whatever
289
+ * the account's answer moved it to. "Still following the default" means the
290
+ * store holds exactly this.
260
291
  *
261
- * ⚠⚠ Every store WRITES the global default when a person chooses in it, and
262
- * that is the point rather than an oversight: `filex.list-sort` is defined as
263
- * "the last sort a person chose", and a choice made in the right-hand pane is
264
- * still a choice. What must not happen — and does not, because the refs are
265
- * per store — is the OTHER pane moving when it does. Measured 2026-09-13:
266
- * sorting the split pane by Size left the main pane on Name ↑ and made Size
267
- * the arrangement the next unconfigured folder opens in.
292
+ * ⚠ Per store, and NOT a fresh read of the first-paint cache: by the time the
293
+ * document is announced, `lib/viewPrefs` has already rewritten that cache with
294
+ * the NEW default, so a comparison against it would see every store as
295
+ * "somebody chose something" and move none of them.
296
+ */
297
+ const followed = new WeakMap<SortStore, { key: SortKey; dir: SortDir }>();
298
+
299
+ /**
300
+ * A fresh sort store, seeded from the DEFAULT (the first-paint cache of it
301
+ * until the account's answer lands).
302
+ *
303
+ * ⚠ A choice made in one store moves only that store's refs: the split pane
304
+ * sorting by Size leaves the main pane on Name ↑ (measured 2026-09-13), and
305
+ * since v0.43 it no longer changes what any other folder opens with either.
268
306
  */
269
307
  export function createSortStore(): SortStore {
270
- const stored = readStored();
308
+ const stored = defaultSort();
271
309
  const key = ref<SortKey>(stored.key);
272
310
  const dir = ref<SortDir>(stored.dir);
273
311
 
274
312
  function setSort(k: SortKey, d?: SortDir): void {
275
313
  key.value = k;
276
314
  dir.value = d ?? defaultSortDir(k);
277
- persist(key.value, dir.value);
278
315
  }
279
316
 
280
317
  /**
281
- * tablo:t1 — RESTORE a sort that was already the person's, without recording
282
- * it as a fresh choice.
283
- *
284
- * ⚠⚠ The difference from `setSort` is the whole point and it is not
285
- * cosmetic. `filex.list-sort` is the GLOBAL default — "what a folder I have
286
- * never set up opens as" — and per-folder memory (`lib/viewPrefs`) is
287
- * layered on top of it. If applying a folder's remembered sort went through
288
- * `setSort`, walking into one folder that is sorted by size would quietly
289
- * make Size the default for every folder in the product, and the person
290
- * would have no idea which folder did it. Restoring is not choosing; only a
291
- * choice writes the default.
318
+ * tablo:t1 — RESTORE a sort (a folder's memory, or the default) without
319
+ * recording it as a fresh choice. The explorer's recorder tells the two
320
+ * apart by comparing against what it last applied, so a restore can never
321
+ * write itself into the folder it was restored into.
292
322
  */
293
323
  function applySort(k: SortKey, d: SortDir): void {
294
324
  key.value = k;
@@ -297,7 +327,6 @@ export function createSortStore(): SortStore {
297
327
 
298
328
  function toggleSortDir(): void {
299
329
  dir.value = dir.value === 'asc' ? 'desc' : 'asc';
300
- persist(key.value, dir.value);
301
330
  }
302
331
 
303
332
  /**
@@ -322,7 +351,7 @@ export function createSortStore(): SortStore {
322
351
  return order === 'relevance' ? byFoldersFirst : compareNodesFn;
323
352
  }
324
353
 
325
- return {
354
+ const store: SortStore = {
326
355
  key,
327
356
  dir,
328
357
  setSort,
@@ -334,6 +363,9 @@ export function createSortStore(): SortStore {
334
363
  sortListing: (files: FileNode[], order: ListingOrder = 'sort') =>
335
364
  [...(files || [])].sort(compareInOrderFn(order)),
336
365
  };
366
+ liveStores.add(store);
367
+ followed.set(store, { ...stored });
368
+ return store;
337
369
  }
338
370
 
339
371
  /**
@@ -387,16 +419,46 @@ export function applySort(key: SortKey, dir: SortDir): void {
387
419
  }
388
420
 
389
421
  /**
390
- * tablo:t1 — THE GLOBAL DEFAULT: the sort a folder nobody has configured opens
391
- * in. Re-read from storage rather than mirrored in a second ref, because
392
- * `setSort` writes there and `applySort` deliberately does not — so the stored
393
- * value IS "the last sort a person chose", which is exactly the definition of
394
- * the default, and a mirror could only drift from it.
422
+ * THE DEFAULT SORT — what a folder nobody has configured opens in: the
423
+ * person's default, else the instance's, else this browser's cached copy of
424
+ * it (until the document lands), else filex's own (Name ↑).
425
+ *
426
+ * ⚠ Once the document has landed the cache is NOT consulted: an account with
427
+ * no default of its own must get filex's default, not whatever the cache held
428
+ * from the last person or the last release.
395
429
  */
396
- export function globalSort(): { key: SortKey; dir: SortDir } {
430
+ export function defaultSort(): { key: SortKey; dir: SortDir } {
431
+ const d = defaultFolderView();
432
+ if (d.k) return { key: d.k, dir: d.d ?? defaultSortDir(d.k) };
433
+ if (viewPrefsReady()) return { key: DEFAULT_KEY, dir: defaultSortDir(DEFAULT_KEY) };
397
434
  return readStored();
398
435
  }
399
436
 
437
+ /** ⚠ DEPRECATED name for `defaultSort` — kept so an embedder importing it
438
+ * keeps compiling. It is no longer "the last sort a person chose". */
439
+ export const globalSort = defaultSort;
440
+
441
+ /**
442
+ * The account's document landed (or another browser's newer one did): move
443
+ * every store that is still following the DEFAULT onto the resolved one.
444
+ *
445
+ * ⚠⚠ "Still following" is the whole subtlety. A store whose sort was restored
446
+ * from a folder's own memory, or chosen in this tab a second ago, must not be
447
+ * yanked — so the move happens only where the store still holds what this
448
+ * browser's cache said, i.e. where nobody has chosen anything since the page
449
+ * opened. (The explorer's main pane is re-applied by the explorer itself,
450
+ * folder memory included; this reaches the other stores.)
451
+ */
452
+ onViewPrefsApplied(() => {
453
+ const want = defaultSort();
454
+ for (const store of liveStores) {
455
+ const was = followed.get(store);
456
+ if (!was || store.key.value !== was.key || store.dir.value !== was.dir) continue;
457
+ store.applySort(want.key, want.dir);
458
+ followed.set(store, { ...want });
459
+ }
460
+ });
461
+
400
462
  export function toggleSortDir(): void {
401
463
  defaultStore.toggleSortDir();
402
464
  }
package/src/lib/star.ts CHANGED
@@ -14,10 +14,14 @@
14
14
  * one). Same trap as `loadStarred`/`fetchNavRows` in FileExplorer.
15
15
  */
16
16
 
17
+ import { requestFailure } from './errorWords';
18
+
17
19
  export interface StarRequestOptions {
18
20
  apiBase?: string;
19
21
  authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
20
22
  authCredentials?: RequestCredentials;
23
+ /** The language a refusal is said in (lib/errorWords). */
24
+ locale?: string;
21
25
  }
22
26
 
23
27
  /** Toggle the starred flag for ONE node. Throws on a non-2xx answer so the
@@ -38,5 +42,7 @@ export async function setNodeStarred(
38
42
  credentials: opts.authCredentials ?? 'same-origin',
39
43
  body: JSON.stringify({ node_id: nodeId, starred }),
40
44
  });
41
- if (!res.ok) throw new Error(`star toggle failed: ${res.status}`);
45
+ // Said (lib/errorWords), not "star toggle failed: 403" — the caller that
46
+ // shows it passes the language it speaks.
47
+ if (!res.ok) throw requestFailure(res.status, await res.text().catch(() => ''), opts.locale);
42
48
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * surfaceCell — a plugin list's cell, as the reader reads it.
3
+ *
4
+ * A `list` column may say what its cells ARE (`format: "date" | "datetime"`),
5
+ * and then the app hands over the machine's value — `2026-09-29`,
6
+ * `2026-09-29T14:03:00Z` — and the host prints it the way the explorer prints
7
+ * every date: in the reader's language, on the reader's clock. The value stays
8
+ * what the column sorts by.
9
+ *
10
+ * ⚠⚠ Why the host and not the app (v0.43.0 wave 2, 2026-09-22): the
11
+ * Signatures page's "Son tarih" read "2026-09-29" a column away from the
12
+ * explorer's "22 Eyl 2026, 14:01". An app formatting dates itself means one
13
+ * more date format per app — the one thing "one format, the explorer's" rules
14
+ * out — and it cannot know the reader's chosen time zone.
15
+ *
16
+ * ⚠ No formatter of its own: a calendar DAY goes through useLocale's
17
+ * `formatCalendarDay` (UTC, so no time zone can move a due date to the day
18
+ * before), an instant through the reader's own `formatDate`. Anything the
19
+ * format does not recognise is shown as it came (a dash, a word).
20
+ */
21
+ import { formatCalendarDay } from '../composables/useLocale';
22
+
23
+ export type SurfaceCellFormat = 'date' | 'datetime';
24
+
25
+ /**
26
+ * The cell's text. `formatInstant` is the reader's own date formatter
27
+ * (useLocale().formatDate), which knows their language AND their clock.
28
+ */
29
+ export function formatSurfaceCell(
30
+ raw: string,
31
+ format: string | undefined,
32
+ locale: string,
33
+ formatInstant: (ms: number, opts?: { time?: boolean }) => string,
34
+ ): string {
35
+ if (!raw || (format !== 'date' && format !== 'datetime')) return raw;
36
+ const day = formatCalendarDay(raw, locale);
37
+ if (day) return day;
38
+ const ms = Date.parse(raw.trim());
39
+ if (Number.isNaN(ms)) return raw;
40
+ return formatInstant(ms, { time: format === 'datetime' }) || raw;
41
+ }