@brftech/filex-core 0.42.1 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +14 -2
  2. package/dist/ArchiveViewer-BqU761Wk.js +149 -0
  3. package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
  4. package/dist/CsvViewer-ByHVK5ga.js +172 -0
  5. package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
  6. package/dist/{DrawioViewer-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
  7. package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
  8. package/dist/{EpubViewer-DS3mb3k8.js → EpubViewer-DJwUKlHW.js} +50 -50
  9. package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
  10. package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
  11. package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
  12. package/dist/MermaidViewer-CEDTD_pp.js +136 -0
  13. package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
  14. package/dist/PsdViewer-CZcxUpZO.js +121 -0
  15. package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
  16. package/dist/TiffViewer-BwI62CbQ.js +141 -0
  17. package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
  18. package/dist/{Viewer3D-CIrOn63I.js → Viewer3D-DoeZcuMm.js} +16 -16
  19. package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
  20. package/dist/filex-core.js +475 -219
  21. package/dist/filex-core.umd.cjs +165 -108
  22. package/dist/filex-core.umd.cjs.map +1 -1
  23. package/dist/fonts/caveat-latin-ext.woff2 +0 -0
  24. package/dist/fonts/caveat-latin.woff2 +0 -0
  25. package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
  26. package/dist/fonts/dancing-script-latin.woff2 +0 -0
  27. package/dist/fonts/homemade-apple-latin.woff2 +0 -0
  28. package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
  29. package/dist/fonts/source-serif-4-latin.woff2 +0 -0
  30. package/dist/index-DuWnGuv5.js +35901 -0
  31. package/dist/index-DuWnGuv5.js.map +1 -0
  32. package/dist/index.d.ts +5241 -553
  33. package/dist/pdf-CaPYsOUo.js +21302 -0
  34. package/dist/pdf-CaPYsOUo.js.map +1 -0
  35. package/dist/style.css +1 -1
  36. package/package.json +1 -1
  37. package/src/FileExplorer.vue +1207 -343
  38. package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
  39. package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
  40. package/src/assets/fonts/README.md +58 -0
  41. package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
  42. package/src/assets/fonts/caveat-latin.woff2 +0 -0
  43. package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
  44. package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
  45. package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
  46. package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
  47. package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
  48. package/src/components/AdvancedSearch.vue +49 -0
  49. package/src/components/Breadcrumb.vue +5 -5
  50. package/src/components/ChoiceButtons.vue +152 -0
  51. package/src/components/CommandPalette.vue +5 -5
  52. package/src/components/ConnectionGuideView.vue +1 -1
  53. package/src/components/ConnectionNotice.vue +59 -0
  54. package/src/components/ConnectionsPanel.vue +30 -568
  55. package/src/components/ContextMenu.vue +31 -5
  56. package/src/components/DataTable.vue +1331 -0
  57. package/src/components/FilePane.vue +3 -0
  58. package/src/components/FilterBar.vue +12 -5
  59. package/src/components/GalleryView.vue +50 -2
  60. package/src/components/GridView.vue +51 -3
  61. package/src/components/HomeView.vue +18 -6
  62. package/src/components/InspectorPanel.vue +134 -17
  63. package/src/components/ListView.vue +366 -900
  64. package/src/components/LogoMark.vue +57 -0
  65. package/src/components/NFSExportsPanel.vue +76 -28
  66. package/src/components/OnboardingTour.vue +19 -7
  67. package/src/components/OperationsCenter.vue +77 -1
  68. package/src/components/PendingOpsTray.vue +59 -5
  69. package/src/components/PresenceBar.vue +7 -7
  70. package/src/components/ProductVersion.vue +31 -0
  71. package/src/components/QuickLook.vue +10 -14
  72. package/src/components/RecentlyOpened.vue +47 -17
  73. package/src/components/RecoveryKeyModal.vue +1 -1
  74. package/src/components/RowActions.vue +134 -0
  75. package/src/components/S3KeysPanel.vue +98 -56
  76. package/src/components/SSHKeysPanel.vue +79 -26
  77. package/src/components/SideNav.vue +260 -96
  78. package/src/components/StarButton.vue +1 -0
  79. package/src/components/StorageFields.vue +74 -6
  80. package/src/components/StorageTags.vue +62 -0
  81. package/src/components/TabBar.vue +1 -1
  82. package/src/components/TagKindIcon.vue +56 -0
  83. package/src/components/TagPicker.vue +226 -40
  84. package/src/components/ThemePalette.vue +4 -4
  85. package/src/components/TimeZonePicker.vue +29 -10
  86. package/src/components/TokensPanel.vue +82 -27
  87. package/src/components/Toolbar.vue +28 -6
  88. package/src/components/plugin/PluginConfirmModal.vue +54 -0
  89. package/src/components/plugin/PluginInspectorSection.vue +149 -0
  90. package/src/components/plugin/PluginPageView.vue +339 -0
  91. package/src/components/plugin/PluginViewModal.vue +159 -0
  92. package/src/components/plugin/SurfaceConversation.vue +76 -0
  93. package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
  94. package/src/components/plugin/SurfaceRenderer.vue +299 -0
  95. package/src/components/plugin/SurfaceSections.vue +91 -0
  96. package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
  97. package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
  98. package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
  99. package/src/components/plugin/nodes/SurfaceList.vue +127 -0
  100. package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
  101. package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
  102. package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
  103. package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
  104. package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
  105. package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
  106. package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
  107. package/src/components/public/PublicLanguagePicker.vue +58 -0
  108. package/src/components/public/PublicLinkPage.vue +286 -0
  109. package/src/components/public/PublicLinkPreview.vue +85 -0
  110. package/src/components/public/PublicPinGate.vue +108 -0
  111. package/src/components/public/PublicRequestBody.vue +204 -0
  112. package/src/components/public/PublicShareBody.vue +188 -0
  113. package/src/components/public/PublicShell.vue +332 -0
  114. package/src/composables/useConnections.ts +8 -2
  115. package/src/composables/useFileApi.ts +183 -44
  116. package/src/composables/useLocale.ts +277 -41
  117. package/src/composables/useNFSExports.ts +8 -10
  118. package/src/composables/useOperations.ts +44 -3
  119. package/src/composables/usePendingOps.ts +59 -8
  120. package/src/composables/usePluginActions.ts +121 -0
  121. package/src/composables/usePluginSurface.ts +223 -0
  122. package/src/composables/usePublicBranding.ts +172 -0
  123. package/src/composables/usePublicLink.ts +573 -0
  124. package/src/composables/usePublicPage.ts +61 -0
  125. package/src/composables/useS3Keys.ts +17 -12
  126. package/src/composables/useSSHKeys.ts +8 -10
  127. package/src/composables/useTokens.ts +8 -12
  128. package/src/composables/useUploadChunked.ts +18 -8
  129. package/src/composables/useViewerFetch.ts +6 -3
  130. package/src/index.ts +404 -8
  131. package/src/lib/actionIcons.ts +46 -1
  132. package/src/lib/advSearch.ts +14 -7
  133. package/src/lib/appLock.ts +130 -0
  134. package/src/lib/connection.ts +110 -0
  135. package/src/lib/destinationTree.ts +19 -3
  136. package/src/lib/direction.ts +353 -0
  137. package/src/lib/dragGhost.ts +6 -0
  138. package/src/lib/dropLimits.ts +69 -0
  139. package/src/lib/errorWords.ts +394 -0
  140. package/src/lib/externalReach.ts +5 -1
  141. package/src/lib/fileFilters.ts +7 -2
  142. package/src/lib/fileIcons.ts +4 -0
  143. package/src/lib/internalPaths.ts +77 -0
  144. package/src/lib/listInput.ts +64 -0
  145. package/src/lib/listing.ts +58 -23
  146. package/src/lib/modalStack.ts +26 -0
  147. package/src/lib/nodeRow.ts +121 -0
  148. package/src/lib/pdfFieldRules.ts +126 -0
  149. package/src/lib/pdfFields.ts +589 -0
  150. package/src/lib/pdfFieldsGeom.ts +203 -0
  151. package/src/lib/pdfjsLoader.ts +51 -0
  152. package/src/lib/personName.ts +47 -0
  153. package/src/lib/pluginApplies.ts +123 -0
  154. package/src/lib/pluginLabel.ts +72 -0
  155. package/src/lib/pluginMenu.ts +253 -0
  156. package/src/lib/pluginPage.ts +58 -0
  157. package/src/lib/plural.ts +181 -0
  158. package/src/lib/popupLayer.ts +103 -0
  159. package/src/lib/prefs.ts +563 -0
  160. package/src/lib/productVersion.ts +35 -0
  161. package/src/lib/publicLayout.ts +39 -0
  162. package/src/lib/serviceGate.ts +104 -0
  163. package/src/lib/shareTtl.ts +25 -11
  164. package/src/lib/signFonts.ts +85 -0
  165. package/src/lib/signaturePad.ts +111 -0
  166. package/src/lib/sortOrder.ts +100 -38
  167. package/src/lib/star.ts +7 -1
  168. package/src/lib/surfaceCell.ts +41 -0
  169. package/src/lib/surfaceConditions.ts +156 -0
  170. package/src/lib/surfaceOpen.ts +106 -0
  171. package/src/lib/surfaceSteps.ts +68 -0
  172. package/src/lib/surfaceValues.ts +191 -0
  173. package/src/lib/symlink.ts +150 -0
  174. package/src/lib/tableColumns.ts +493 -0
  175. package/src/lib/tableEnv.ts +34 -0
  176. package/src/lib/tablePrefs.ts +84 -0
  177. package/src/lib/tags.ts +121 -12
  178. package/src/lib/themes.ts +286 -18
  179. package/src/lib/tour.ts +152 -0
  180. package/src/lib/trashEmpty.ts +111 -0
  181. package/src/lib/trashTimeLeft.ts +23 -0
  182. package/src/lib/uiLocales.ts +460 -0
  183. package/src/lib/viewPrefs.ts +696 -660
  184. package/src/lib/viewPrefsHttp.ts +78 -0
  185. package/src/locales/en.ts +522 -119
  186. package/src/locales/resolve.ts +17 -8
  187. package/src/locales/tr.ts +539 -155
  188. package/src/modals/ConvertModal.vue +62 -11
  189. package/src/modals/DestinationPickerModal.vue +59 -16
  190. package/src/modals/Modal.vue +113 -23
  191. package/src/modals/NewDocumentModal.vue +16 -6
  192. package/src/modals/NewFolderModal.vue +8 -0
  193. package/src/modals/PermissionsModal.vue +272 -163
  194. package/src/modals/PreviewModal.vue +119 -20
  195. package/src/modals/RenameModal.vue +37 -1
  196. package/src/styles/base.css +2895 -241
  197. package/src/styles/sign-fonts.css +118 -0
  198. package/src/styles/variables.css +60 -0
  199. package/src/types/Connections.ts +17 -0
  200. package/src/types/ExplorerConfig.ts +117 -1
  201. package/src/types/FileNode.ts +58 -0
  202. package/src/types/Plugins.ts +494 -0
  203. package/src/types/Public.ts +212 -0
  204. package/src/viewers/ArchiveViewer.vue +93 -48
  205. package/src/viewers/CsvViewer.vue +99 -53
  206. package/src/viewers/DrawioViewer.vue +35 -9
  207. package/src/viewers/EpubViewer.vue +7 -8
  208. package/src/viewers/IpynbViewer.vue +3 -1
  209. package/src/viewers/MermaidViewer.vue +4 -5
  210. package/src/viewers/PdfViewer.vue +8 -17
  211. package/src/viewers/PsdViewer.vue +2 -4
  212. package/src/viewers/TiffViewer.vue +4 -5
  213. package/src/viewers/Viewer3D.vue +1 -1
  214. package/LICENSE +0 -21
  215. package/dist/ArchiveViewer-CLNevfPE.js +0 -124
  216. package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
  217. package/dist/CsvViewer-K8XA3z7N.js +0 -141
  218. package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
  219. package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
  220. package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
  221. package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
  222. package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
  223. package/dist/MermaidViewer-DqucH_io.js +0 -137
  224. package/dist/MermaidViewer-DqucH_io.js.map +0 -1
  225. package/dist/PsdViewer-CwiVENmq.js +0 -122
  226. package/dist/PsdViewer-CwiVENmq.js.map +0 -1
  227. package/dist/TiffViewer-CpewSVmQ.js +0 -142
  228. package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
  229. package/dist/Viewer3D-CIrOn63I.js.map +0 -1
  230. package/dist/index-Dx0IbBcI.js +0 -26831
  231. package/dist/index-Dx0IbBcI.js.map +0 -1
  232. package/dist/useViewerFetch-czqbd2Lj.js +0 -25
  233. package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
@@ -0,0 +1,493 @@
1
+ /**
2
+ * tableColumns — THE column model of the one table this product has.
3
+ *
4
+ * ⚠⚠ Read this before writing anything tabular. filex has exactly ONE table:
5
+ * the explorer's list view, extracted into `components/DataTable.vue`. Every
6
+ * table in the product — the explorer's own listing, every admin page, the
7
+ * connection panels, My shares, the notifications list and an app's `list`
8
+ * node — renders through that component, and every one of them therefore gets
9
+ * what the explorer's list can do: resize a column, sort by it, hide it, move
10
+ * it, and have all of that remembered.
11
+ *
12
+ * The owner, 2026-09-21, after a round that had built a SECOND table which
13
+ * imitated the explorer (the same pinned edges, the same Actions menu) and left
14
+ * everything else behind: "Admin tabloları hâlâ explore tablolarıyla AYNI
15
+ * KODDA DEĞİL. LÜTFEN AYNI KODA ALALIM. Örnek veriyorum: tablo sütunları
16
+ * düzenlenebilir değil, büyütme küçültme yok, sıralama yok." An imitation gets
17
+ * the parts somebody remembered to copy. The same code gets all of it, and
18
+ * keeps getting whatever is added next.
19
+ *
20
+ * This file is the half of that table that has no DOM: which columns exist,
21
+ * how wide each one is, which ones are hidden, in what order they stand, and
22
+ * how wide the whole row therefore is. It used to live inside `lib/viewPrefs`
23
+ * as the explorer's private column state, typed to the explorer's seven
24
+ * columns; it is written here once, generic over the column ids, and the
25
+ * explorer is one caller of it among many.
26
+ *
27
+ * ⚠ It holds NO state of its own. A store is handed a BACKING — get the
28
+ * state, replace the state — and the backing decides where that lives: the
29
+ * explorer's lives in the per-folder view document (`lib/viewPrefs`), every
30
+ * other table's in the same per-person document under its own table id
31
+ * (`lib/tablePrefs`). That split is the reason the arithmetic can be shared
32
+ * without the storage being shared.
33
+ */
34
+ import { computed, shallowRef } from 'vue';
35
+
36
+ /** One column the table knows how to draw. */
37
+ export interface TableColumnSpec<Id extends string = string> {
38
+ id: Id;
39
+ /** Shipped width in px. */
40
+ width: number;
41
+ min: number;
42
+ max: number;
43
+ /** May the person hide it from the column menu? Also what makes it MOVABLE:
44
+ * a column the person cannot hide is pinned where the table put it (the
45
+ * explorer's Name first, its star last). */
46
+ hideable: boolean;
47
+ /** May the person drag its edge? */
48
+ resizable: boolean;
49
+ }
50
+
51
+ /**
52
+ * What is remembered about one table's columns. Short keys on purpose — the
53
+ * explorer keeps one of these per folder and the whole document is
54
+ * re-serialised on every save.
55
+ */
56
+ export interface ColsState<Id extends string = string> {
57
+ /** Widths the person set, px. A column absent here is at its shipped width. */
58
+ w: Partial<Record<Id, number>>;
59
+ /** Columns the person switched off. */
60
+ hidden: Id[];
61
+ /** The person's order. Absent = the order the columns were declared in. */
62
+ o?: Id[];
63
+ }
64
+
65
+ export function emptyCols<Id extends string = string>(): ColsState<Id> {
66
+ return { w: {}, hidden: [] };
67
+ }
68
+
69
+ /**
70
+ * Read a stored column state against the columns that exist TODAY.
71
+ *
72
+ * ⚠ Clamped and filtered on the way IN, not only on the way out: a width
73
+ * stored by an older build (or hand-edited) outside today's bounds would draw
74
+ * a column nobody can reach the edge of, and a hidden id that no longer names
75
+ * a column would sit in the document forever. Nothing in the UI could explain
76
+ * either, so neither is allowed through.
77
+ */
78
+ export function readCols<Id extends string>(
79
+ raw: unknown,
80
+ specs: readonly TableColumnSpec<Id>[],
81
+ ): ColsState<Id> {
82
+ const fallback = emptyCols<Id>();
83
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return fallback;
84
+ const specOf = (id: unknown) => specs.find((c) => c.id === id);
85
+ const p = raw as { w?: unknown; hidden?: unknown; o?: unknown };
86
+ const w: Partial<Record<Id, number>> = {};
87
+ if (p.w && typeof p.w === 'object') {
88
+ for (const [k, v] of Object.entries(p.w as Record<string, unknown>)) {
89
+ const spec = specOf(k);
90
+ if (!spec || typeof v !== 'number' || !Number.isFinite(v)) continue;
91
+ w[spec.id] = Math.min(spec.max, Math.max(spec.min, Math.round(v)));
92
+ }
93
+ }
94
+ const hidden = Array.isArray(p.hidden)
95
+ ? [...new Set(p.hidden.filter((x) => specOf(x)?.hideable) as Id[])]
96
+ : [];
97
+ const o = Array.isArray(p.o) ? ([...new Set(p.o.filter((x) => !!specOf(x)))] as Id[]) : undefined;
98
+ return o && o.length ? { w, hidden, o } : { w, hidden };
99
+ }
100
+
101
+ /**
102
+ * THE REORDER ARITHMETIC, on its own and with no state attached.
103
+ *
104
+ * ⚠⚠ `to` is the index the item ENDS UP AT — equivalently, the insertion index
105
+ * in the list with the item already taken out. The two readings coincide for
106
+ * every `to`, and that equivalence is the whole point of writing it down:
107
+ * every reorder bug this function exists to close was a caller mixing the two
108
+ * index spaces. A caller that computes its target against the list WITH the
109
+ * column still in it hands over an index one too large whenever the column
110
+ * moves right — which is exactly how "drag one column to the right" became
111
+ * "two columns to the right". (Owner, 2026-09-20: "bir adım ileri çekince iki
112
+ * adım ilerliyor".)
113
+ *
114
+ * `from === to` is a no-op and returns a copy, so a drop on the column's own
115
+ * place writes nothing.
116
+ */
117
+ export function reorderColumns<T>(list: readonly T[], from: number, to: number): T[] {
118
+ const next = [...list];
119
+ if (from < 0 || from >= next.length) return next;
120
+ const target = Math.max(0, Math.min(next.length - 1, to));
121
+ if (target === from) return next;
122
+ const [item] = next.splice(from, 1);
123
+ next.splice(target, 0, item);
124
+ return next;
125
+ }
126
+
127
+ /**
128
+ * The stretch of the order a movable column may occupy — `[lo, hi]`, both
129
+ * inclusive indices into `order`.
130
+ *
131
+ * ⚠ The pinned columns are not just un-draggable, they are also un-passable:
132
+ * the lead is the row's handle and stays first, the explorer's star is a
133
+ * control that lives beside the row menu and stays last. Measured 2026-09-20:
134
+ * dropping Type at the end of the drawn columns put it PAST the star, so the
135
+ * header read `… Size ★ Type ⋮` and the Type label stood over a 24px control
136
+ * column. The band is derived rather than hard-coded so a pinned column at
137
+ * either end of ANY table is honoured without another rule remembering to.
138
+ */
139
+ export function columnDropBand<Id extends string>(
140
+ order: readonly Id[],
141
+ specs: readonly TableColumnSpec<Id>[],
142
+ ): { lo: number; hi: number } {
143
+ const movable = (id: Id) => specs.find((c) => c.id === id)?.hideable === true;
144
+ let lo = 0;
145
+ while (lo < order.length && !movable(order[lo])) lo++;
146
+ let hi = order.length - 1;
147
+ while (hi >= 0 && !movable(order[hi])) hi--;
148
+ return { lo, hi: Math.max(lo, hi) };
149
+ }
150
+
151
+ /** Where a store reads and writes its state. */
152
+ export interface ColumnBacking<Id extends string = string> {
153
+ /** The stored state, or anything that does not parse as one (read as empty). */
154
+ get(): unknown;
155
+ /** Replace it. The store always hands over a complete, normalised state. */
156
+ set(next: ColsState<Id>): void;
157
+ /**
158
+ * Is there anything here the person could RESET? Optional — by default,
159
+ * "the state says something". The explorer's folder backing answers "does
160
+ * THIS folder have columns of its own", because a folder that shows the
161
+ * person's default columns has nothing of its own to reset, and a "Reset
162
+ * columns" row that does nothing when clicked reads as broken.
163
+ */
164
+ customised?(): boolean;
165
+ }
166
+
167
+ /** What one table looks like right now. */
168
+ export interface TableLayout<Id extends string = string> {
169
+ /** The columns drawn after the lead, in the person's order. Never shortened
170
+ * for width — a table wider than its pane scrolls. */
171
+ visible: Id[];
172
+ /** Every drawn track's width in px, the lead's included. */
173
+ widths: Record<Id, number>;
174
+ /** The table's own width: every track, every gap, both paddings. */
175
+ total: number;
176
+ /** True while the widths are derived from the pane rather than stored. */
177
+ auto: boolean;
178
+ }
179
+
180
+ export interface LayoutMetrics {
181
+ /** Space between cells (`--fe-gap-sm`). */
182
+ gap?: number;
183
+ /** Row padding, both sides together (`--fe-gap` × 2). */
184
+ padding?: number;
185
+ /** The tick column's track, or 0 for a table with no tick. */
186
+ check?: number;
187
+ /** The trailing actions column's track (the explorer's ⋮ is 28). */
188
+ menu?: number;
189
+ /** What the lead opens at when nobody has sized anything. */
190
+ leadAuto?: number;
191
+ }
192
+
193
+ export interface ColumnStore<Id extends string = string> {
194
+ readonly lead: Id;
195
+ specs(): readonly TableColumnSpec<Id>[];
196
+ spec(id: Id): TableColumnSpec<Id> | undefined;
197
+ /** The full order, lead first, reconciled with the columns that exist. */
198
+ order(): Id[];
199
+ width(id: Id): number;
200
+ setWidth(id: Id, px: number): void;
201
+ hidden(id: Id): boolean;
202
+ setHidden(id: Id, hidden: boolean): void;
203
+ /** Put `id` at `index` in the order — `index` is where it ENDS UP. */
204
+ move(id: Id, index: number): void;
205
+ /** Has anybody ever sized this table? (the lead's width is the bit) */
206
+ widthsAreAuto(): boolean;
207
+ /** Commit the widths on screen for every column that has none stored. */
208
+ freeze(widths: Partial<Record<Id, number>>): void;
209
+ /** Back to the shipped widths, order and visibility. */
210
+ reset(): void;
211
+ /** Has anything about the columns been changed by hand? */
212
+ customised(): boolean;
213
+ /** The state as it resolves right now — what "Apply to all folders" copies. */
214
+ state(): ColsState<Id>;
215
+ layout(available: number, candidates: readonly Id[], metrics?: LayoutMetrics): TableLayout<Id>;
216
+ }
217
+
218
+ /**
219
+ * A column store over `specs`, reading and writing through `backing`.
220
+ *
221
+ * `lead` is the column pinned first: never hidden, never moved, always
222
+ * resizable. It is the one that says WHICH ROW this is — the explorer's Name,
223
+ * an admin table's email or path — and the table freezes it on the left edge
224
+ * while the others scroll.
225
+ *
226
+ * ⚠ The state is read through a `computed`, so a store created inside a
227
+ * component re-renders it when the backing changes (the explorer's backing
228
+ * changes every time the folder does) and parses the stored document once per
229
+ * change rather than once per cell.
230
+ */
231
+ export function createColumnStore<Id extends string>(
232
+ specsOf: () => readonly TableColumnSpec<Id>[],
233
+ backing: ColumnBacking<Id>,
234
+ opts: { lead?: Id } = {},
235
+ ): ColumnStore<Id> {
236
+ const lead = (opts.lead ?? specsOf()[0]?.id) as Id;
237
+ const state = computed<ColsState<Id>>(() => readCols(backing.get(), specsOf()));
238
+ const specOf = (id: Id) => specsOf().find((c) => c.id === id);
239
+
240
+ function write(next: ColsState<Id>): void {
241
+ backing.set(next);
242
+ }
243
+
244
+ /**
245
+ * THE COLUMN ORDER — the person's, reconciled with the columns that exist.
246
+ *
247
+ * ⚠⚠ Reconciled, not trusted. A stored order was written by an older build,
248
+ * so it can be missing a column added since and can name one removed since.
249
+ * Returning it as-is would make a new column INVISIBLE to everyone who had
250
+ * ever touched the order — a feature that ships and then does not appear for
251
+ * exactly the users engaged enough to have customised something.
252
+ *
253
+ * ⚠ The lead first, whatever the document says: it is pinned in the table,
254
+ * so an order that claimed otherwise would be a sequence nothing on screen
255
+ * obeys.
256
+ *
257
+ * ⚠ A column the stored list never heard of goes to the END, not to its own
258
+ * shipped index. Measured: inserting at the shipped index walked newcomers in
259
+ * FRONT of the columns the person had deliberately moved — somebody who had
260
+ * put Size first would open a later release to find two new columns ahead of
261
+ * it and their arrangement silently rewritten.
262
+ */
263
+ function order(): Id[] {
264
+ const known = specsOf().map((c) => c.id);
265
+ const stored = state.value.o;
266
+ if (!stored || stored.length === 0) {
267
+ return [lead, ...known.filter((id) => id !== lead)];
268
+ }
269
+ const out = stored.filter((id) => known.includes(id) && id !== lead);
270
+ out.unshift(lead);
271
+ for (const id of known) if (!out.includes(id)) out.push(id);
272
+ return out;
273
+ }
274
+
275
+ function width(id: Id): number {
276
+ const spec = specOf(id);
277
+ if (!spec) return 0;
278
+ return state.value.w[id] ?? spec.width;
279
+ }
280
+
281
+ function setWidth(id: Id, px: number): void {
282
+ const spec = specOf(id);
283
+ if (!spec || !spec.resizable) return;
284
+ const next = Math.min(spec.max, Math.max(spec.min, Math.round(px)));
285
+ if (state.value.w[id] === next) return;
286
+ write({ ...state.value, w: { ...state.value.w, [id]: next } });
287
+ }
288
+
289
+ /**
290
+ * HAS ANYBODY EVER SIZED THIS TABLE — the one bit that separates "I have
291
+ * never touched this" from "I deliberately made the lead narrow".
292
+ *
293
+ * ⚠⚠ Without it the two are indistinguishable and the first resize is undone
294
+ * by the next pane resize: an untouched table is sized to the pane it is in,
295
+ * so a person who drags the lead to 140 and then opens a side panel would get
296
+ * it recomputed back to whatever the narrower pane suggests. The bit is the
297
+ * LEAD's width rather than "the map is empty": documents written before the
298
+ * explorer's Name became a sized column carry widths but have never been
299
+ * asked about Name, and reading those as "configured" would open the table
300
+ * at a width nobody chose. The first drag writes every width at once
301
+ * (`freeze`), so a table somebody has actually sized can never fall back.
302
+ */
303
+ function widthsAreAuto(): boolean {
304
+ return state.value.w[lead] === undefined;
305
+ }
306
+
307
+ /**
308
+ * ⚠⚠ Called at the START of a resize gesture, and it has to write EVERY
309
+ * drawn column, not the one being dragged. Writing only the dragged column
310
+ * would leave the lead auto — and the auto lead takes the slack, so
311
+ * narrowing Size would widen Name, which is the exact behaviour the owner
312
+ * called wrong ("küçültme yapınca name kısmı büyüyormuş gibi davranıyor").
313
+ */
314
+ function freeze(widths: Partial<Record<Id, number>>): void {
315
+ const w: Partial<Record<Id, number>> = { ...state.value.w };
316
+ let changed = false;
317
+ for (const [k, v] of Object.entries(widths) as [Id, number | undefined][]) {
318
+ if (typeof v !== 'number' || !Number.isFinite(v)) continue;
319
+ if (w[k] !== undefined) continue;
320
+ const spec = specOf(k);
321
+ if (!spec || !spec.resizable) continue;
322
+ w[k] = Math.min(spec.max, Math.max(spec.min, Math.round(v)));
323
+ changed = true;
324
+ }
325
+ if (changed) write({ ...state.value, w });
326
+ }
327
+
328
+ function hidden(id: Id): boolean {
329
+ return state.value.hidden.includes(id);
330
+ }
331
+
332
+ function setHidden(id: Id, hide: boolean): void {
333
+ const spec = specOf(id);
334
+ if (!spec || !spec.hideable || id === lead) return;
335
+ if (hidden(id) === hide) return;
336
+ const set = new Set(state.value.hidden);
337
+ if (hide) set.add(id);
338
+ else set.delete(id);
339
+ write({ ...state.value, hidden: [...set] });
340
+ }
341
+
342
+ function move(id: Id, index: number): void {
343
+ const spec = specOf(id);
344
+ /* A column the person cannot hide is not theirs to move either: it is
345
+ pinned where the table put it (the lead first, a control column last). */
346
+ if (!spec || !spec.hideable || id === lead) return;
347
+ const cur = order();
348
+ const from = cur.indexOf(id);
349
+ if (from === -1) return;
350
+ const band = columnDropBand(cur, specsOf());
351
+ const to = Math.max(band.lo, Math.min(band.hi, index));
352
+ const next = reorderColumns(cur, from, to);
353
+ if (next.join() === cur.join()) return;
354
+ write({ ...state.value, o: next });
355
+ }
356
+
357
+ function reset(): void {
358
+ write(emptyCols<Id>());
359
+ }
360
+
361
+ function customised(): boolean {
362
+ if (backing.customised) return backing.customised();
363
+ return (
364
+ Object.keys(state.value.w).length > 0 ||
365
+ state.value.hidden.length > 0 ||
366
+ (state.value.o?.length ?? 0) > 0
367
+ );
368
+ }
369
+
370
+ /**
371
+ * THE TABLE'S WIDTHS — one answer, used by the header and by every row.
372
+ *
373
+ * · nothing is ever dropped for want of room — `visible` is the person's
374
+ * choice and the caller's candidates, and width has no vote. The owner
375
+ * rejected the table that shed columns and said "no room": "no room
376
+ * demesin, kenara devam eden bir scroll getirsin";
377
+ * · the table's width is the SUM of its columns, so when that exceeds the
378
+ * pane the table scrolls sideways instead of shrinking;
379
+ * · with nothing stored the widths are derived from the pane, so a table
380
+ * nobody has configured opens sensibly at 1440 and at 390 alike: the lead
381
+ * asks for `leadAuto`, the others give up room toward their minimums to
382
+ * pay for it, and the lead takes any slack that is left. The instant one
383
+ * width is stored the pane stops having an opinion.
384
+ *
385
+ * ⚠ `candidates` is what the CALLER is willing to draw at all — the
386
+ * explorer's Location only where rows come from more than one folder, its
387
+ * star only where starring is offered. Passing them keeps those facts with
388
+ * the component that knows them.
389
+ */
390
+ function layout(
391
+ available: number,
392
+ candidates: readonly Id[],
393
+ metrics: LayoutMetrics = {},
394
+ ): TableLayout<Id> {
395
+ const gap = metrics.gap ?? 8;
396
+ const padding = metrics.padding ?? 24;
397
+ const check = metrics.check ?? 0;
398
+ const menu = metrics.menu ?? 28;
399
+ const leadSpec = specOf(lead);
400
+
401
+ const visible = order().filter((id) => id !== lead && candidates.includes(id) && !hidden(id));
402
+
403
+ const widths = {} as Record<Id, number>;
404
+ for (const id of visible) widths[id] = width(id);
405
+ widths[lead] = width(lead);
406
+
407
+ /* Cells: [tick] + lead + the others + the actions column, so one gap
408
+ fewer than that. */
409
+ const cells = (check > 0 ? 1 : 0) + 1 + visible.length + 1;
410
+ const chrome = check + menu + padding + gap * (cells - 1);
411
+ const auto = widthsAreAuto();
412
+
413
+ /* A container we cannot measure yet (0 on the first tick, or a test with
414
+ no layout) keeps the shipped widths — the observer corrects it one frame
415
+ later, and a table that guessed would flash. */
416
+ if (auto && available > 0 && leadSpec) {
417
+ const leadAuto = metrics.leadAuto ?? leadSpec.width;
418
+ const room = available - chrome;
419
+ let rest = visible.reduce((s, id) => s + widths[id], 0);
420
+ const target = room - leadAuto;
421
+ if (rest > target) {
422
+ const give = visible.reduce((s, id) => s + (widths[id] - (specOf(id)?.min ?? widths[id])), 0);
423
+ if (give > 0) {
424
+ const f = Math.min(1, (rest - target) / give);
425
+ for (const id of visible) {
426
+ const min = specOf(id)?.min ?? widths[id];
427
+ /* ⚠ `ceil` on the reduction, not `floor`: rounding the other way
428
+ leaves the row a pixel or two over the pane and hands an
429
+ otherwise fitting table a horizontal scrollbar on first sight —
430
+ measured at 960, 712 in a 711px pane. */
431
+ widths[id] = Math.max(min, widths[id] - Math.ceil((widths[id] - min) * f));
432
+ }
433
+ rest = visible.reduce((s, id) => s + widths[id], 0);
434
+ }
435
+ }
436
+ /* Slack goes to the lead, and only to the lead — and where there is
437
+ none to give, the lead gives way too, down to its own minimum.
438
+
439
+ ⚠⚠ It used to stop at `leadAuto`, and `leadAuto` is a DESKTOP
440
+ default (240px for a table that names nothing else). In a 265px side
441
+ panel that IS the pane: an app's list drew its Identity column and
442
+ nothing else, and the State and When columns it was sent sat under
443
+ the pinned Actions cell, so a signer table read as names, buttons,
444
+ and a blank gap between them (v0.43.0). Standing at 240 does not
445
+ make anything fit — the table is wider than the pane either way and
446
+ scrolls, which is the owner's rule — it only decides what the person
447
+ is shown FIRST, and one wide column plus nothing is the worse
448
+ answer. Nothing is ever dropped here; the lead simply stops taking
449
+ the whole pane. */
450
+ const leadFloor = Math.min(leadAuto, leadSpec.min);
451
+ widths[lead] = Math.round(Math.max(leadFloor, Math.min(leadSpec.max, room - rest)));
452
+ }
453
+
454
+ const total = chrome + widths[lead] + visible.reduce((s, id) => s + widths[id], 0);
455
+ return { visible, widths, total, auto };
456
+ }
457
+
458
+ return {
459
+ lead,
460
+ specs: specsOf,
461
+ spec: specOf,
462
+ order,
463
+ width,
464
+ setWidth,
465
+ hidden,
466
+ setHidden,
467
+ move,
468
+ widthsAreAuto,
469
+ freeze,
470
+ reset,
471
+ customised,
472
+ state: () => state.value,
473
+ layout,
474
+ };
475
+ }
476
+
477
+ /**
478
+ * A backing that lives in this component's memory only — for a table on a
479
+ * surface with no account behind it (a public page), and for tests.
480
+ */
481
+ export function memoryBacking<Id extends string = string>(
482
+ initial: ColsState<Id> = emptyCols<Id>(),
483
+ ): ColumnBacking<Id> {
484
+ /* A ref, or the store's `computed` would never see a write and the table
485
+ would not move when a column is dragged. */
486
+ const box = shallowRef<ColsState<Id>>(initial);
487
+ return {
488
+ get: () => box.value,
489
+ set: (next) => {
490
+ box.value = next;
491
+ },
492
+ };
493
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * tableEnv — the two things every table needs from its HOST and no view
3
+ * should have to pass by hand: the interface language and the light/dark
4
+ * mode.
5
+ *
6
+ * ⚠ Why an injection and not two props on every table. The admin app draws
7
+ * some thirty tables; each one needs the language (the column menu, "Resize
8
+ * {col}", the pager, the Actions label) and the light/dark mode (the column
9
+ * menu and the Actions menu teleport to <body>, outside the chrome that paints
10
+ * with `.dark`, so they have to be told). Threading two props through thirty
11
+ * call sites is thirty places for one of them to be forgotten — which is how
12
+ * one table ends up with an English column menu inside a Turkish page. The
13
+ * host provides it ONCE (web/src/App.vue); a prop on a table still wins, and
14
+ * with neither the table falls back to English and the browser's own mode.
15
+ */
16
+ import { inject, provide, type InjectionKey, type Ref } from 'vue';
17
+ import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
18
+
19
+ export interface TableEnv {
20
+ locale: Ref<LocaleCode>;
21
+ theme?: Ref<ThemeMode | undefined>;
22
+ }
23
+
24
+ export const TABLE_ENV: InjectionKey<TableEnv> = Symbol('fe-table-env');
25
+
26
+ /** Called once by a host, in its root component's setup. */
27
+ export function provideTableEnv(env: TableEnv): void {
28
+ provide(TABLE_ENV, env);
29
+ }
30
+
31
+ /** The host's answer, or null. Must be called during `setup()`. */
32
+ export function useTableEnv(): TableEnv | null {
33
+ return inject(TABLE_ENV, null);
34
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * tablePrefs — where every table that is NOT the explorer's listing keeps its
3
+ * columns and its sort.
4
+ *
5
+ * One corner of the per-person view document (`lib/viewPrefs` →
6
+ * `viewPrefsSlot('t')`), keyed by a table id the table is given:
7
+ *
8
+ * { "t": { "admin.users": { "c": {w, hidden, o}, "s": {k, d} }, … } }
9
+ *
10
+ * ⚠⚠ On the ACCOUNT, like the explorer's folders, and for the same reason the
11
+ * owner gave for those ("tarayıcıya değil db'ye kaydedeceğiz"): a column
12
+ * dragged wider in the Users table on the office machine is there at home,
13
+ * and a second person signing into the same browser does not inherit it.
14
+ * With nobody signed in (a public page, an app token) the slot is
15
+ * session-only — the table still resizes and sorts, it just forgets on reload.
16
+ *
17
+ * ⚠ The id is the table's name, not its route: two tables on one page (the
18
+ * Replica screen draws three) need three ids, and a table that moves to
19
+ * another page keeps its arrangement. `web/tests/ui/tablePinnedActions.test.ts`
20
+ * refuses a table with no id and two tables with the same one.
21
+ */
22
+ import { viewPrefsSlot } from './viewPrefs';
23
+ import type { ColsState, ColumnBacking } from './tableColumns';
24
+
25
+ /** One table's remembered state. */
26
+ export interface TablePrefs {
27
+ c?: ColsState;
28
+ s?: { k: string; d: 'asc' | 'desc' };
29
+ }
30
+
31
+ type TableMap = Record<string, TablePrefs>;
32
+
33
+ /**
34
+ * The slot is created lazily and ONCE: `viewPrefsSlot` throws for a reserved
35
+ * name, and calling it at module load would make importing this file a side
36
+ * effect in every bundle that never draws a table.
37
+ */
38
+ let slot: ReturnType<typeof viewPrefsSlot<TableMap>> | null = null;
39
+ function tables() {
40
+ if (!slot) slot = viewPrefsSlot<TableMap>('t');
41
+ return slot;
42
+ }
43
+
44
+ function read(): TableMap {
45
+ const v = tables().get();
46
+ return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
47
+ }
48
+
49
+ function write(id: string, patch: Partial<TablePrefs>): void {
50
+ const all = read();
51
+ const cur: TablePrefs = { ...(all[id] ?? {}) };
52
+ for (const [k, v] of Object.entries(patch) as [keyof TablePrefs, unknown][]) {
53
+ if (v === undefined) delete cur[k];
54
+ else (cur as Record<string, unknown>)[k] = v;
55
+ }
56
+ const next = { ...all };
57
+ if (Object.keys(cur).length) next[id] = cur;
58
+ else delete next[id];
59
+ tables().set(next);
60
+ }
61
+
62
+ /** The backing a `ColumnStore` for table `id` reads and writes through. */
63
+ export function tableColumnBacking(id: string): ColumnBacking {
64
+ return {
65
+ get: () => read()[id]?.c,
66
+ set: (next) => {
67
+ const says =
68
+ Object.keys(next.w).length > 0 || next.hidden.length > 0 || (next.o?.length ?? 0) > 0;
69
+ write(id, { c: says ? next : undefined });
70
+ },
71
+ };
72
+ }
73
+
74
+ /** The table's remembered sort, or null. */
75
+ export function tableSort(id: string): { key: string; dir: 'asc' | 'desc' } | null {
76
+ const s = read()[id]?.s;
77
+ if (!s || typeof s.k !== 'string' || (s.d !== 'asc' && s.d !== 'desc')) return null;
78
+ return { key: s.k, dir: s.d };
79
+ }
80
+
81
+ /** Remember (or with null, forget) the table's sort. */
82
+ export function setTableSort(id: string, sort: { key: string; dir: 'asc' | 'desc' } | null): void {
83
+ write(id, { s: sort ? { k: sort.key, d: sort.dir } : undefined });
84
+ }