@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.
- package/README.md +14 -2
- package/dist/ArchiveViewer-BqU761Wk.js +149 -0
- package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
- package/dist/CsvViewer-ByHVK5ga.js +172 -0
- package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
- package/dist/{DrawioViewer-CC6rJXeP.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-CAoTSXTO.js → EpubViewer-DJwUKlHW.js} +50 -50
- package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
- package/dist/MermaidViewer-CEDTD_pp.js +136 -0
- package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
- package/dist/PsdViewer-CZcxUpZO.js +121 -0
- package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
- package/dist/TiffViewer-BwI62CbQ.js +141 -0
- package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
- package/dist/{Viewer3D-CdZeK4j4.js → Viewer3D-DoeZcuMm.js} +16 -16
- package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
- package/dist/filex-core.js +475 -219
- package/dist/filex-core.umd.cjs +165 -108
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/fonts/caveat-latin-ext.woff2 +0 -0
- package/dist/fonts/caveat-latin.woff2 +0 -0
- package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/dist/fonts/dancing-script-latin.woff2 +0 -0
- package/dist/fonts/homemade-apple-latin.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/index-DuWnGuv5.js +35901 -0
- package/dist/index-DuWnGuv5.js.map +1 -0
- package/dist/index.d.ts +5227 -553
- package/dist/pdf-CaPYsOUo.js +21302 -0
- package/dist/pdf-CaPYsOUo.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +1055 -253
- package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
- package/src/assets/fonts/README.md +58 -0
- package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
- package/src/assets/fonts/caveat-latin.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
- package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
- package/src/components/AdvancedSearch.vue +49 -0
- package/src/components/Breadcrumb.vue +5 -5
- package/src/components/ChoiceButtons.vue +152 -0
- package/src/components/CommandPalette.vue +5 -5
- package/src/components/ConnectionGuideView.vue +1 -1
- package/src/components/ConnectionNotice.vue +59 -0
- package/src/components/ConnectionsPanel.vue +30 -568
- package/src/components/ContextMenu.vue +31 -5
- package/src/components/DataTable.vue +1331 -0
- package/src/components/FilePane.vue +3 -0
- package/src/components/FilterBar.vue +12 -5
- package/src/components/GalleryView.vue +50 -2
- package/src/components/GridView.vue +51 -3
- package/src/components/HomeView.vue +18 -6
- package/src/components/InspectorPanel.vue +134 -17
- package/src/components/ListView.vue +366 -900
- package/src/components/LogoMark.vue +57 -0
- package/src/components/NFSExportsPanel.vue +76 -28
- package/src/components/OnboardingTour.vue +19 -7
- package/src/components/OperationsCenter.vue +77 -1
- package/src/components/PendingOpsTray.vue +59 -5
- package/src/components/PresenceBar.vue +7 -7
- package/src/components/ProductVersion.vue +31 -0
- package/src/components/QuickLook.vue +10 -14
- package/src/components/RecentlyOpened.vue +31 -15
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -62
- package/src/components/SSHKeysPanel.vue +79 -30
- package/src/components/SideNav.vue +260 -96
- package/src/components/StarButton.vue +1 -0
- package/src/components/StorageFields.vue +74 -6
- package/src/components/StorageTags.vue +62 -0
- package/src/components/TabBar.vue +1 -1
- package/src/components/TagKindIcon.vue +56 -0
- package/src/components/TagPicker.vue +226 -40
- package/src/components/ThemePalette.vue +4 -4
- package/src/components/TimeZonePicker.vue +29 -10
- package/src/components/TokensPanel.vue +82 -31
- package/src/components/Toolbar.vue +28 -6
- package/src/components/plugin/PluginConfirmModal.vue +54 -0
- package/src/components/plugin/PluginInspectorSection.vue +149 -0
- package/src/components/plugin/PluginPageView.vue +339 -0
- package/src/components/plugin/PluginViewModal.vue +159 -0
- package/src/components/plugin/SurfaceConversation.vue +76 -0
- package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
- package/src/components/plugin/SurfaceRenderer.vue +299 -0
- package/src/components/plugin/SurfaceSections.vue +91 -0
- package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
- package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
- package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
- package/src/components/plugin/nodes/SurfaceList.vue +127 -0
- package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
- package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
- package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
- package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
- package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
- package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
- package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
- package/src/components/public/PublicLanguagePicker.vue +58 -0
- package/src/components/public/PublicLinkPage.vue +286 -0
- package/src/components/public/PublicLinkPreview.vue +85 -0
- package/src/components/public/PublicPinGate.vue +108 -0
- package/src/components/public/PublicRequestBody.vue +204 -0
- package/src/components/public/PublicShareBody.vue +188 -0
- package/src/components/public/PublicShell.vue +332 -0
- package/src/composables/useConnections.ts +8 -2
- package/src/composables/useFileApi.ts +183 -44
- package/src/composables/useLocale.ts +277 -41
- package/src/composables/useNFSExports.ts +8 -10
- package/src/composables/useOperations.ts +44 -3
- package/src/composables/usePendingOps.ts +59 -8
- package/src/composables/usePluginActions.ts +121 -0
- package/src/composables/usePluginSurface.ts +223 -0
- package/src/composables/usePublicBranding.ts +172 -0
- package/src/composables/usePublicLink.ts +573 -0
- package/src/composables/usePublicPage.ts +61 -0
- package/src/composables/useS3Keys.ts +17 -12
- package/src/composables/useSSHKeys.ts +8 -10
- package/src/composables/useTokens.ts +8 -12
- package/src/composables/useUploadChunked.ts +18 -8
- package/src/composables/useViewerFetch.ts +6 -3
- package/src/index.ts +404 -8
- package/src/lib/actionIcons.ts +46 -1
- package/src/lib/advSearch.ts +14 -7
- package/src/lib/appLock.ts +130 -0
- package/src/lib/connection.ts +110 -0
- package/src/lib/destinationTree.ts +19 -3
- package/src/lib/direction.ts +353 -0
- package/src/lib/dragGhost.ts +6 -0
- package/src/lib/dropLimits.ts +69 -0
- package/src/lib/errorWords.ts +394 -0
- package/src/lib/externalReach.ts +5 -1
- package/src/lib/fileFilters.ts +7 -2
- package/src/lib/fileIcons.ts +4 -0
- package/src/lib/internalPaths.ts +77 -0
- package/src/lib/listInput.ts +64 -0
- package/src/lib/listing.ts +58 -23
- package/src/lib/modalStack.ts +26 -0
- package/src/lib/nodeRow.ts +18 -3
- package/src/lib/pdfFieldRules.ts +126 -0
- package/src/lib/pdfFields.ts +589 -0
- package/src/lib/pdfFieldsGeom.ts +203 -0
- package/src/lib/pdfjsLoader.ts +51 -0
- package/src/lib/personName.ts +47 -0
- package/src/lib/pluginApplies.ts +123 -0
- package/src/lib/pluginLabel.ts +72 -0
- package/src/lib/pluginMenu.ts +253 -0
- package/src/lib/pluginPage.ts +58 -0
- package/src/lib/plural.ts +181 -0
- package/src/lib/popupLayer.ts +103 -0
- package/src/lib/prefs.ts +563 -0
- package/src/lib/productVersion.ts +35 -0
- package/src/lib/publicLayout.ts +39 -0
- package/src/lib/serviceGate.ts +104 -0
- package/src/lib/shareTtl.ts +25 -11
- package/src/lib/signFonts.ts +85 -0
- package/src/lib/signaturePad.ts +111 -0
- package/src/lib/sortOrder.ts +100 -38
- package/src/lib/star.ts +7 -1
- package/src/lib/surfaceCell.ts +41 -0
- package/src/lib/surfaceConditions.ts +156 -0
- package/src/lib/surfaceOpen.ts +106 -0
- package/src/lib/surfaceSteps.ts +68 -0
- package/src/lib/surfaceValues.ts +191 -0
- package/src/lib/symlink.ts +150 -0
- package/src/lib/tableColumns.ts +493 -0
- package/src/lib/tableEnv.ts +34 -0
- package/src/lib/tablePrefs.ts +84 -0
- package/src/lib/tags.ts +121 -12
- package/src/lib/themes.ts +286 -18
- package/src/lib/tour.ts +152 -0
- package/src/lib/trashEmpty.ts +111 -0
- package/src/lib/trashTimeLeft.ts +23 -0
- package/src/lib/uiLocales.ts +460 -0
- package/src/lib/viewPrefs.ts +696 -660
- package/src/lib/viewPrefsHttp.ts +78 -0
- package/src/locales/en.ts +515 -117
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +536 -152
- package/src/modals/ConvertModal.vue +62 -11
- package/src/modals/DestinationPickerModal.vue +59 -16
- package/src/modals/Modal.vue +113 -23
- package/src/modals/NewDocumentModal.vue +16 -6
- package/src/modals/NewFolderModal.vue +8 -0
- package/src/modals/PermissionsModal.vue +272 -163
- package/src/modals/PreviewModal.vue +119 -20
- package/src/modals/RenameModal.vue +37 -1
- package/src/styles/base.css +2890 -260
- package/src/styles/sign-fonts.css +118 -0
- package/src/styles/variables.css +60 -0
- package/src/types/Connections.ts +17 -0
- package/src/types/ExplorerConfig.ts +117 -1
- package/src/types/FileNode.ts +50 -0
- package/src/types/Plugins.ts +494 -0
- package/src/types/Public.ts +212 -0
- package/src/viewers/ArchiveViewer.vue +93 -48
- package/src/viewers/CsvViewer.vue +99 -53
- package/src/viewers/DrawioViewer.vue +35 -9
- package/src/viewers/EpubViewer.vue +7 -8
- package/src/viewers/IpynbViewer.vue +3 -1
- package/src/viewers/MermaidViewer.vue +4 -5
- package/src/viewers/PdfViewer.vue +8 -17
- package/src/viewers/PsdViewer.vue +2 -4
- package/src/viewers/TiffViewer.vue +4 -5
- package/src/viewers/Viewer3D.vue +1 -1
- package/LICENSE +0 -21
- package/dist/ArchiveViewer-BUQWLhwX.js +0 -124
- package/dist/ArchiveViewer-BUQWLhwX.js.map +0 -1
- package/dist/CsvViewer-Bg2_aqzc.js +0 -141
- package/dist/CsvViewer-Bg2_aqzc.js.map +0 -1
- package/dist/DrawioViewer-CC6rJXeP.js.map +0 -1
- package/dist/EpubViewer-CAoTSXTO.js.map +0 -1
- package/dist/IpynbViewer-D0GwGpnE.js +0 -184
- package/dist/IpynbViewer-D0GwGpnE.js.map +0 -1
- package/dist/MermaidViewer-DGwAvRZg.js +0 -137
- package/dist/MermaidViewer-DGwAvRZg.js.map +0 -1
- package/dist/PsdViewer-DhGweA0m.js +0 -122
- package/dist/PsdViewer-DhGweA0m.js.map +0 -1
- package/dist/TiffViewer-D0AurvAh.js +0 -142
- package/dist/TiffViewer-D0AurvAh.js.map +0 -1
- package/dist/Viewer3D-CdZeK4j4.js.map +0 -1
- package/dist/index-BArI-B1M.js +0 -26911
- package/dist/index-BArI-B1M.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
- package/src/composables/useScrolledX.ts +0 -21
package/src/lib/prefs.ts
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prefs — the look-and-language preferences, kept on the ACCOUNT.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ Why this module exists: a palette picked in one browser was not there
|
|
5
|
+
* in the next one. Theme, palette, density and language were each stored in
|
|
6
|
+
* `localStorage` and nowhere else, so every device — and every private
|
|
7
|
+
* window, and every cleared cache — started the product over with somebody
|
|
8
|
+
* else's defaults. They are a PERSON's preferences, not a machine's.
|
|
9
|
+
*
|
|
10
|
+
* So the answer lives on the account: `GET /api/me/prefs?surface=web` on
|
|
11
|
+
* boot, `PUT /api/me/prefs?surface=web` on every change, debounced.
|
|
12
|
+
*
|
|
13
|
+
* ⚠ `localStorage` stays, and stays for exactly ONE job: the first paint.
|
|
14
|
+
* The fetch cannot beat the first frame, so the mirror is what stops the
|
|
15
|
+
* window flashing the stock palette on the way to the chosen one. It is a
|
|
16
|
+
* CACHE of the account's answer, never the answer — when the server's
|
|
17
|
+
* values land they overwrite it.
|
|
18
|
+
*
|
|
19
|
+
* ⚠⚠ ONE module, several surfaces. The web app asks for `surface=web` and
|
|
20
|
+
* the desktop app for `surface=desktop` — same code, same keys, same
|
|
21
|
+
* debounce; only the query differs, because a person may legitimately want
|
|
22
|
+
* a dense list on a laptop and a roomy one on a wall screen. There is no
|
|
23
|
+
* per-surface BRANCH anywhere in here, and there must not be one: a
|
|
24
|
+
* behaviour that exists on one surface only is the bug this product keeps
|
|
25
|
+
* having to un-write.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The preferences an account carries for one surface. */
|
|
29
|
+
export interface UiPrefs {
|
|
30
|
+
/** `light` | `dark` | `auto`. */
|
|
31
|
+
theme?: string;
|
|
32
|
+
/** A palette id from `lib/themes` (`default`, `night-blue`, …). */
|
|
33
|
+
palette?: string;
|
|
34
|
+
/** `comfortable` | `compact`. */
|
|
35
|
+
density?: string;
|
|
36
|
+
/** A language tag the interface offers (`lib/uiLocales`). */
|
|
37
|
+
locale?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Set (to `done`) once the first-use tour has been OFFERED to this person
|
|
40
|
+
* (`lib/tour`). Not a look: it has no first-paint mirror of its own, and it
|
|
41
|
+
* only ever goes one way — nothing in the product clears it.
|
|
42
|
+
*/
|
|
43
|
+
tour?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type PrefKey = keyof UiPrefs;
|
|
47
|
+
|
|
48
|
+
/** The keys that paint the window — each has a first-paint mirror (below). */
|
|
49
|
+
export type LookKey = Exclude<PrefKey, 'tour'>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every key the account document carries.
|
|
53
|
+
*
|
|
54
|
+
* ⚠⚠ A key the client does not list here is DROPPED on the next write:
|
|
55
|
+
* `sanitize` keeps only these, and `PUT` replaces the whole document. So a
|
|
56
|
+
* preference that is not a look (the tour) still has to be named here, or the
|
|
57
|
+
* first palette change after it was stored would erase it.
|
|
58
|
+
*/
|
|
59
|
+
export const PREF_KEYS: readonly PrefKey[] = ['theme', 'palette', 'density', 'locale', 'tour'];
|
|
60
|
+
|
|
61
|
+
/** The keys with a first-paint mirror in `localStorage` — the look, nothing else. */
|
|
62
|
+
export const LOOK_KEYS: readonly LookKey[] = ['theme', 'palette', 'density', 'locale'];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The localStorage mirror's key per preference.
|
|
66
|
+
*
|
|
67
|
+
* ⚠⚠ These are the names the surfaces ALREADY read at first paint
|
|
68
|
+
* (`web/src/lib/theme`, `lib/themes.THEME_LS_KEY`, `web/src/lib/density`,
|
|
69
|
+
* `web/src/i18n`). Inventing new ones here would have left four first-paint
|
|
70
|
+
* readers looking at keys nobody writes — the preference would work, and
|
|
71
|
+
* the window would flash on every load, which is the failure this module is
|
|
72
|
+
* supposed to remove.
|
|
73
|
+
*/
|
|
74
|
+
export const PREF_LS_KEYS: Record<LookKey, string> = {
|
|
75
|
+
theme: 'filex.theme',
|
|
76
|
+
palette: 'filex.palette',
|
|
77
|
+
density: 'filex.density',
|
|
78
|
+
locale: 'filex.locale',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** How long a change waits for the next one before it is sent. */
|
|
82
|
+
export const PREFS_PUT_DEBOUNCE_MS = 400;
|
|
83
|
+
|
|
84
|
+
/* ── is anybody signed in? ────────────────────────────────────────────── */
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* ⚠⚠ WITH NO SESSION, NOTHING PERSONAL PAINTS. The owner's rule, verbatim:
|
|
88
|
+
* "logoutluyken zaten seçtiğim tema değil, şirketin default, yoksa filex'in
|
|
89
|
+
* default teması gelecek; tarayıcı gece modundaysa gece modunda olacak ya da
|
|
90
|
+
* light mode."
|
|
91
|
+
*
|
|
92
|
+
* So a signed-out window wears the INSTANCE's look (`ui.default_theme`, or
|
|
93
|
+
* filex's own stock palette when the operator has not set one) and follows the
|
|
94
|
+
* BROWSER's `prefers-color-scheme`. The palette and the light/dark mode became
|
|
95
|
+
* per-PERSON settings in v3 (migration 00047, `GET/PUT /api/me/prefs`), and a
|
|
96
|
+
* person is exactly what a signed-out window does not have.
|
|
97
|
+
*
|
|
98
|
+
* ⚠ Measured before it was changed (2026-09-21, vitest against the shipped
|
|
99
|
+
* `packages/core/dist`): with `filex.palette` = `night` and no session at all,
|
|
100
|
+
* `useThemeState().themeId` came back `"night"` and `filex.thememode` = `dark`
|
|
101
|
+
* came back `"dark"` — both read at MODULE LOAD, long before anything could
|
|
102
|
+
* know whether a session existed. Worse, `instanceThemes.hasOwnChoice()` saw
|
|
103
|
+
* the same key and suppressed the operator's own default entirely, so an
|
|
104
|
+
* instance with a brand theme showed the LAST PERSON WHO SIGNED IN HERE their
|
|
105
|
+
* colours on its sign-in page — and showed the next person at that shared
|
|
106
|
+
* machine the previous one's taste.
|
|
107
|
+
*
|
|
108
|
+
* ⚠⚠ WHY A STORED HINT AND NOT THE REAL ANSWER. The real answer is
|
|
109
|
+
* `/api/auth/me`, and it cannot be had at the first paint: the session cookie
|
|
110
|
+
* is HttpOnly (handlers/auth.go) so JavaScript cannot see it, and the fetch
|
|
111
|
+
* resolves long after the window has painted. A wrong answer is a visible bug
|
|
112
|
+
* in BOTH directions — read the personal palette with no session and the leak
|
|
113
|
+
* above is back; ignore it WITH a session and every single load flashes the
|
|
114
|
+
* instance default on the way to the person's own palette. So this is a guess
|
|
115
|
+
* that is right in the overwhelming case (this browser's last load answered
|
|
116
|
+
* the same question) and is corrected within one round trip by
|
|
117
|
+
* `rememberSession`, which every entry path reaches through `/api/auth/me`.
|
|
118
|
+
*/
|
|
119
|
+
export const SESSION_LS_KEY = 'filex.session';
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Set for THIS DOCUMENT only, never persisted: a public link (`/s/`, `/d/`)
|
|
123
|
+
* cannot carry a session whatever the browser remembers.
|
|
124
|
+
*
|
|
125
|
+
* ⚠ Persisting it would be the bug. The same browser's admin tab reads the
|
|
126
|
+
* same localStorage, so writing "no session" from a share link would make that
|
|
127
|
+
* tab flash the instance default on its next load — the very flash the mirror
|
|
128
|
+
* exists to prevent.
|
|
129
|
+
*/
|
|
130
|
+
let sealed = false;
|
|
131
|
+
|
|
132
|
+
function readSessionHint(): boolean {
|
|
133
|
+
try {
|
|
134
|
+
return localStorage.getItem(SESSION_LS_KEY) === '1';
|
|
135
|
+
} catch {
|
|
136
|
+
// Private window / blocked site data. "No session" is the safe answer: it
|
|
137
|
+
// shows the instance's look, never somebody else's.
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Is a person signed in here? The synchronous, first-paint answer.
|
|
144
|
+
*
|
|
145
|
+
* ⚠ `true` for a host that has not declared `sessionAware` — see that
|
|
146
|
+
* option. "Nobody told us about sessions" means "this explorer is inside
|
|
147
|
+
* something that already dealt with it", not "nobody is here".
|
|
148
|
+
*/
|
|
149
|
+
export function hasSession(): boolean {
|
|
150
|
+
if (sealed) return false;
|
|
151
|
+
if (!cfg.sessionAware) return true;
|
|
152
|
+
return readSessionHint();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `/api/auth/me` has answered. Remember it for this browser's next first paint.
|
|
157
|
+
*
|
|
158
|
+
* ⚠ A hint and nothing more — it is not a credential, it grants nothing, and
|
|
159
|
+
* every authenticated request still carries the HttpOnly cookie. Forging it
|
|
160
|
+
* buys an attacker the wrong palette.
|
|
161
|
+
*/
|
|
162
|
+
export function rememberSession(present: boolean): void {
|
|
163
|
+
try {
|
|
164
|
+
if (present) localStorage.setItem(SESSION_LS_KEY, '1');
|
|
165
|
+
else localStorage.removeItem(SESSION_LS_KEY);
|
|
166
|
+
} catch {
|
|
167
|
+
/* quota / private mode — `hasSession()` then answers false, which is the
|
|
168
|
+
safe direction */
|
|
169
|
+
}
|
|
170
|
+
if (present) sealed = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** This document is a public link: the answer is no, and it is not remembered. */
|
|
174
|
+
export function sealSessionless(): void {
|
|
175
|
+
sealed = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Every localStorage key that mirrors a PERSON and must not outlive their
|
|
180
|
+
* session.
|
|
181
|
+
*
|
|
182
|
+
* ⚠⚠ An allow-list, never `localStorage.clear()`. Most of what this product
|
|
183
|
+
* keeps in a browser is a fact about the MACHINE or the INSTALL and survives a
|
|
184
|
+
* sign-out on purpose:
|
|
185
|
+
* • `filex.instancetheme` — the operator's default, a PUBLIC fact identical
|
|
186
|
+
* for everybody. Clearing it would put back the very flash it was added to
|
|
187
|
+
* remove, on the sign-in page, for nothing.
|
|
188
|
+
* • `filex.installPrompt.dismissed`, `filex.notify.browserAsked`,
|
|
189
|
+
* `filex.tourDone` — "this browser has already been asked". Clearing them
|
|
190
|
+
* re-nags whoever sits down next, which is worse than useless. (The tour
|
|
191
|
+
* is ALSO recorded on the account now — `lib/tour` — and the two are read
|
|
192
|
+
* as one answer: seen here OR seen by this person anywhere.)
|
|
193
|
+
* • `filex.sidenav`, `filex.inspector`, `filex.tabs`, `filex.showHidden`,
|
|
194
|
+
* `filex.startpage`, `filex.shortcuts`, `filex.presence.expanded` — window
|
|
195
|
+
* furniture and this machine's ergonomics, not a look somebody wears.
|
|
196
|
+
* • `filex.openTrigger`, `filex.desktopHandoff` — in-flight machinery for a
|
|
197
|
+
* hand-off that may be mid-sign-in right now.
|
|
198
|
+
* `filex.bearer` is already dropped by the auth store, and it lives in
|
|
199
|
+
* sessionStorage besides.
|
|
200
|
+
*/
|
|
201
|
+
const personalMirrors = new Set<string>(Object.values(PREF_LS_KEYS));
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A module with a per-person key of its own adds it here.
|
|
205
|
+
*
|
|
206
|
+
* ⚠ `lib/themes` registers `filex.thememode` this way rather than having its
|
|
207
|
+
* name repeated in the list above — a second copy of a storage key is exactly
|
|
208
|
+
* the drift `lib/density`' header warns about, where a rename on one side
|
|
209
|
+
* leaves the other writing to a key nobody reads.
|
|
210
|
+
*/
|
|
211
|
+
export function registerPersonalMirror(key: string): void {
|
|
212
|
+
personalMirrors.add(key);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The session ended: this browser forgets everything it cached ABOUT THE
|
|
217
|
+
* PERSON.
|
|
218
|
+
*
|
|
219
|
+
* Owner, 2026-09-21: *"oturum kapanınca temizlersek localstorage'ı tamamız ya
|
|
220
|
+
* o kısımda"* — once somebody has left, their preferences are not something
|
|
221
|
+
* this machine should go on holding.
|
|
222
|
+
*
|
|
223
|
+
* ⚠⚠ It is an "and", not an "instead of". Clearing only covers the moments
|
|
224
|
+
* the app KNOWS about; it cannot cover a cookie that quietly expired, a tab
|
|
225
|
+
* closed without signing out, or a browser whose owner never signs out at all.
|
|
226
|
+
* `hasSession()` is what covers those, by making the keys unreadable rather
|
|
227
|
+
* than absent. Drop either one and the sign-in page goes back to wearing the
|
|
228
|
+
* last person who used this browser.
|
|
229
|
+
*
|
|
230
|
+
* ⚠ The in-memory document goes too, and the queued PUT with it. Leaving
|
|
231
|
+
* `remote` behind would keep `hasOwnChoice()` answering for somebody who has
|
|
232
|
+
* gone; leaving `pending` behind would fire a write with their values a moment
|
|
233
|
+
* after their cookie stopped existing.
|
|
234
|
+
*/
|
|
235
|
+
export function forgetPersonalPrefs(): void {
|
|
236
|
+
rememberSession(false);
|
|
237
|
+
for (const key of personalMirrors) {
|
|
238
|
+
try {
|
|
239
|
+
localStorage.removeItem(key);
|
|
240
|
+
} catch {
|
|
241
|
+
/* blocked site data — `hasSession()` already makes the key unreadable */
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
remote = {};
|
|
245
|
+
hydrated = false;
|
|
246
|
+
pending = {};
|
|
247
|
+
if (timer !== null) clearTimeout(timer);
|
|
248
|
+
timer = null;
|
|
249
|
+
// The answer to "whose preferences?" is "nobody's": nothing more is coming
|
|
250
|
+
// for this document until somebody signs in (and `hydratePrefs` runs again).
|
|
251
|
+
settle();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface PrefsConfig {
|
|
255
|
+
/** `web`, `desktop`, or whatever a new surface calls itself. */
|
|
256
|
+
surface: string;
|
|
257
|
+
/** API origin; empty = same origin. */
|
|
258
|
+
base?: string;
|
|
259
|
+
/** Per-call auth, for a host that does not ride on cookies (the desktop app). */
|
|
260
|
+
headers?: () => Promise<Record<string, string>> | Record<string, string>;
|
|
261
|
+
fetchImpl?: typeof fetch;
|
|
262
|
+
/**
|
|
263
|
+
* ⚠⚠ Does this host have a SIGNED-OUT state at all?
|
|
264
|
+
*
|
|
265
|
+
* The filex SPA does: it draws a sign-in page, and the rule this flag turns
|
|
266
|
+
* on is the owner's (`SESSION_LS_KEY` above) — with nobody signed in, the
|
|
267
|
+
* palette and the light/dark mode in this browser belong to somebody else
|
|
268
|
+
* and are not read.
|
|
269
|
+
*
|
|
270
|
+
* Most hosts do NOT. An explorer embedded in work.example.com or in the fishapp
|
|
271
|
+
* is mounted inside a page the host already authenticated; the desktop app
|
|
272
|
+
* is past its pairing screen. They never call `rememberSession`, they have
|
|
273
|
+
* no `/api/auth/me` to call, and gating them would mean the palette a person
|
|
274
|
+
* picked inside the embed silently stopped applying on the next load — a
|
|
275
|
+
* regression, not a fix.
|
|
276
|
+
*
|
|
277
|
+
* ⚠ This is a fact about the HOST, declared once beside `surface` and
|
|
278
|
+
* `base`, not a branch on which surface is running: there is exactly one
|
|
279
|
+
* mechanism and every caller goes through it. Default OFF, so a host that
|
|
280
|
+
* says nothing keeps the behaviour it has always had.
|
|
281
|
+
*/
|
|
282
|
+
sessionAware?: boolean;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let cfg: PrefsConfig = { surface: 'web' };
|
|
286
|
+
let remote: UiPrefs = {};
|
|
287
|
+
/** The server has answered at least once. */
|
|
288
|
+
let hydrated = false;
|
|
289
|
+
/** A host said where the account's document lives (`configurePrefs`). */
|
|
290
|
+
let configured = false;
|
|
291
|
+
/**
|
|
292
|
+
* The question "whose preferences are these?" has been answered for this
|
|
293
|
+
* document: the server's copy arrived, or the fetch gave up, or the session
|
|
294
|
+
* turned out to be nobody's (`forgetPersonalPrefs`). Something that must not
|
|
295
|
+
* decide before the account has spoken (the first-use tour) waits for this,
|
|
296
|
+
* not for `hydrated` — a fetch that failed never hydrates.
|
|
297
|
+
*/
|
|
298
|
+
let settled = false;
|
|
299
|
+
|
|
300
|
+
const listeners = new Set<(p: UiPrefs) => void>();
|
|
301
|
+
const settleWaiters = new Set<() => void>();
|
|
302
|
+
|
|
303
|
+
export function configurePrefs(next: PrefsConfig): void {
|
|
304
|
+
cfg = { ...next };
|
|
305
|
+
configured = true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Did the host wire the account's document at all? An explorer embedded in
|
|
310
|
+
* another product never calls `configurePrefs`, and for it there is no account
|
|
311
|
+
* copy to wait for — only this browser's.
|
|
312
|
+
*/
|
|
313
|
+
export function prefsConfigured(): boolean {
|
|
314
|
+
return configured;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** See `settled`. */
|
|
318
|
+
export function prefsSettled(): boolean {
|
|
319
|
+
return settled;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Be told once the account's answer is in (or known not to be coming). Returns the unsubscribe. */
|
|
323
|
+
export function onPrefsSettled(fn: () => void): () => void {
|
|
324
|
+
if (settled) {
|
|
325
|
+
fn();
|
|
326
|
+
return () => {};
|
|
327
|
+
}
|
|
328
|
+
settleWaiters.add(fn);
|
|
329
|
+
return () => settleWaiters.delete(fn);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function settle(): void {
|
|
333
|
+
settled = true;
|
|
334
|
+
const waiting = [...settleWaiters];
|
|
335
|
+
settleWaiters.clear();
|
|
336
|
+
for (const fn of waiting) {
|
|
337
|
+
try {
|
|
338
|
+
fn();
|
|
339
|
+
} catch {
|
|
340
|
+
/* one waiter's mistake is not another's */
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The preferences as they stand — the server's answer once it has landed. */
|
|
346
|
+
export function currentPrefs(): UiPrefs {
|
|
347
|
+
return { ...remote };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function prefsHydrated(): boolean {
|
|
351
|
+
return hydrated;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Be told when the account's answer arrives or changes. Returns the unsubscribe. */
|
|
355
|
+
export function onPrefs(fn: (p: UiPrefs) => void): () => void {
|
|
356
|
+
listeners.add(fn);
|
|
357
|
+
return () => listeners.delete(fn);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function announce(): void {
|
|
361
|
+
const snapshot = currentPrefs();
|
|
362
|
+
for (const fn of listeners) {
|
|
363
|
+
try {
|
|
364
|
+
fn(snapshot);
|
|
365
|
+
} catch {
|
|
366
|
+
/* one listener's mistake is not another's */
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* ── the first-paint mirror ───────────────────────────────────────────── */
|
|
372
|
+
|
|
373
|
+
/** What this DEVICE last saw — read before the network, never trusted after it. */
|
|
374
|
+
export function localPref(key: LookKey): string {
|
|
375
|
+
try {
|
|
376
|
+
return localStorage.getItem(PREF_LS_KEYS[key]) ?? '';
|
|
377
|
+
} catch {
|
|
378
|
+
// Private window, blocked site data: the caller's own default is the
|
|
379
|
+
// answer, and a preference is never worth an exception.
|
|
380
|
+
return '';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function setLocalPref(key: LookKey, value: string): void {
|
|
385
|
+
try {
|
|
386
|
+
if (value) localStorage.setItem(PREF_LS_KEYS[key], value);
|
|
387
|
+
else localStorage.removeItem(PREF_LS_KEYS[key]);
|
|
388
|
+
} catch {
|
|
389
|
+
/* see above */
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Everything the mirror holds, for the boot that has no server answer yet. */
|
|
394
|
+
export function localPrefs(): UiPrefs {
|
|
395
|
+
const out: UiPrefs = {};
|
|
396
|
+
for (const k of LOOK_KEYS) {
|
|
397
|
+
const v = localPref(k);
|
|
398
|
+
if (v) out[k] = v;
|
|
399
|
+
}
|
|
400
|
+
return out;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* ── the account's copy ───────────────────────────────────────────────── */
|
|
404
|
+
|
|
405
|
+
function url(): string {
|
|
406
|
+
const base = (cfg.base ?? '').replace(/\/+$/, '');
|
|
407
|
+
return `${base}/api/me/prefs?surface=${encodeURIComponent(cfg.surface)}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function headers(withBody: boolean): Promise<Record<string, string>> {
|
|
411
|
+
const h: Record<string, string> = { Accept: 'application/json' };
|
|
412
|
+
if (withBody) h['Content-Type'] = 'application/json';
|
|
413
|
+
// ⚠ awaited — the desktop app's token is a FUNCTION that resolves
|
|
414
|
+
// asynchronously, and spreading the promise sends no Authorization at all.
|
|
415
|
+
const extra = cfg.headers ? await cfg.headers() : {};
|
|
416
|
+
return { ...h, ...extra };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Only the keys this client knows, only as strings — a server that grows another is ignored, not obeyed. */
|
|
420
|
+
function sanitize(raw: unknown): UiPrefs {
|
|
421
|
+
const src = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
|
|
422
|
+
const body = (src.prefs && typeof src.prefs === 'object' ? src.prefs : src) as Record<string, unknown>;
|
|
423
|
+
const out: UiPrefs = {};
|
|
424
|
+
for (const k of PREF_KEYS) {
|
|
425
|
+
const v = body[k];
|
|
426
|
+
if (typeof v === 'string' && v) out[k] = v;
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Read the account's preferences.
|
|
433
|
+
*
|
|
434
|
+
* Answers `null` when there is nothing to apply — no session, an older
|
|
435
|
+
* server with no such route, a network that is not there. ⚠ `null` is not an
|
|
436
|
+
* error: the mirror has already painted the window, and a preference is the
|
|
437
|
+
* last thing that should put an error in front of a person.
|
|
438
|
+
*/
|
|
439
|
+
export async function hydratePrefs(): Promise<UiPrefs | null> {
|
|
440
|
+
const doFetch = cfg.fetchImpl ?? (typeof fetch === 'function' ? fetch : null);
|
|
441
|
+
if (!doFetch) {
|
|
442
|
+
settle();
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
// ⚠ A new question is being asked. A sign-out settled the last one with
|
|
446
|
+
// "nobody's" (`forgetPersonalPrefs`), and a person who then signs in on the
|
|
447
|
+
// sign-in page — no reload — must not be decided for on that stale answer
|
|
448
|
+
// while their own document is still on its way.
|
|
449
|
+
settled = false;
|
|
450
|
+
try {
|
|
451
|
+
const res = await doFetch(url(), {
|
|
452
|
+
method: 'GET',
|
|
453
|
+
headers: await headers(false),
|
|
454
|
+
credentials: 'same-origin',
|
|
455
|
+
cache: 'no-store',
|
|
456
|
+
});
|
|
457
|
+
if (!res.ok) return null;
|
|
458
|
+
remote = sanitize(await res.json());
|
|
459
|
+
hydrated = true;
|
|
460
|
+
// ⚠⚠ An account that has NEVER stored anything adopts what this browser
|
|
461
|
+
// already had, instead of wiping it. Every one of these preferences lived
|
|
462
|
+
// in localStorage alone until v3, so the first load after the upgrade
|
|
463
|
+
// would otherwise meet an empty document, clear the mirror and hand the
|
|
464
|
+
// person back the stock palette they had chosen their way out of — a
|
|
465
|
+
// silent reset that looks exactly like the bug this module was written to
|
|
466
|
+
// fix. One key already stored is enough to make the ACCOUNT the answer:
|
|
467
|
+
// somebody who cleared a palette on another device has a document, and
|
|
468
|
+
// nothing here resurrects what they cleared.
|
|
469
|
+
if (!Object.keys(remote).length) {
|
|
470
|
+
const mine = localPrefs();
|
|
471
|
+
if (Object.keys(mine).length) {
|
|
472
|
+
remote = mine;
|
|
473
|
+
for (const k of LOOK_KEYS) if (mine[k]) pending[k] = mine[k];
|
|
474
|
+
if (timer !== null) clearTimeout(timer);
|
|
475
|
+
timer = setTimeout(() => {
|
|
476
|
+
inFlight = flush();
|
|
477
|
+
}, PREFS_PUT_DEBOUNCE_MS);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// The mirror is a cache of THIS answer from here on.
|
|
481
|
+
for (const k of LOOK_KEYS) setLocalPref(k, remote[k] ?? '');
|
|
482
|
+
announce();
|
|
483
|
+
return currentPrefs();
|
|
484
|
+
} catch {
|
|
485
|
+
return null;
|
|
486
|
+
} finally {
|
|
487
|
+
settle();
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let pending: UiPrefs = {};
|
|
492
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
493
|
+
let inFlight: Promise<void> | null = null;
|
|
494
|
+
|
|
495
|
+
async function flush(): Promise<void> {
|
|
496
|
+
timer = null;
|
|
497
|
+
const changed = pending;
|
|
498
|
+
pending = {};
|
|
499
|
+
if (!Object.keys(changed).length) return;
|
|
500
|
+
// ⚠⚠ The WHOLE document, wrapped in `{prefs: …}`. `PUT /api/me/prefs`
|
|
501
|
+
// REPLACES the stored document (handlers/userprefs.go: "Put replaces the
|
|
502
|
+
// caller's document for one surface") — sending only what changed would
|
|
503
|
+
// drop every other preference the account holds, which is a data loss that
|
|
504
|
+
// looks like "my palette reset itself".
|
|
505
|
+
const body = { prefs: currentPrefs() };
|
|
506
|
+
const doFetch = cfg.fetchImpl ?? (typeof fetch === 'function' ? fetch : null);
|
|
507
|
+
if (!doFetch) return;
|
|
508
|
+
try {
|
|
509
|
+
await doFetch(url(), {
|
|
510
|
+
method: 'PUT',
|
|
511
|
+
headers: await headers(true),
|
|
512
|
+
credentials: 'same-origin',
|
|
513
|
+
body: JSON.stringify(body),
|
|
514
|
+
});
|
|
515
|
+
} catch {
|
|
516
|
+
/* the change is already on screen and in the mirror; a failed write
|
|
517
|
+
means the next device does not see it, not that this one breaks */
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* A preference changed: mirror it now, tell the account shortly.
|
|
523
|
+
*
|
|
524
|
+
* ⚠ The mirror is written FIRST and unconditionally. If the PUT fails, this
|
|
525
|
+
* device still opens the way the person left it — which is exactly the
|
|
526
|
+
* behaviour the product had before the account copy existed.
|
|
527
|
+
*/
|
|
528
|
+
export function savePref(key: PrefKey, value: string): void {
|
|
529
|
+
remote = { ...remote, [key]: value };
|
|
530
|
+
if (key !== 'tour') setLocalPref(key, value);
|
|
531
|
+
pending = { ...pending, [key]: value };
|
|
532
|
+
if (timer !== null) clearTimeout(timer);
|
|
533
|
+
timer = setTimeout(() => {
|
|
534
|
+
inFlight = flush();
|
|
535
|
+
}, PREFS_PUT_DEBOUNCE_MS);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Send anything still waiting, now (a window closing, a test). */
|
|
539
|
+
export async function flushPrefs(): Promise<void> {
|
|
540
|
+
if (timer !== null) {
|
|
541
|
+
clearTimeout(timer);
|
|
542
|
+
inFlight = flush();
|
|
543
|
+
}
|
|
544
|
+
await inFlight;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Testing seam: forget the session's state. */
|
|
548
|
+
export function resetPrefs(): void {
|
|
549
|
+
remote = {};
|
|
550
|
+
hydrated = false;
|
|
551
|
+
// ⚠ The document seal too. It is per-DOCUMENT state and a test file is one
|
|
552
|
+
// document: a spec that opened a public link would otherwise leave every
|
|
553
|
+
// later spec in this file believing it was on one.
|
|
554
|
+
sealed = false;
|
|
555
|
+
pending = {};
|
|
556
|
+
if (timer !== null) clearTimeout(timer);
|
|
557
|
+
timer = null;
|
|
558
|
+
inFlight = null;
|
|
559
|
+
listeners.clear();
|
|
560
|
+
settled = false;
|
|
561
|
+
settleWaiters.clear();
|
|
562
|
+
configured = false;
|
|
563
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* productVersion — the one line that says which filex this is: `filex 0.43.0`.
|
|
3
|
+
*
|
|
4
|
+
* Burak, 2026-09-24: the version should be somewhere a PERSON can find it —
|
|
5
|
+
* the account menu, or user settings. It was only on the sign-in page (twice,
|
|
6
|
+
* each written out by hand) and on the administrators' About page, so anybody
|
|
7
|
+
* who is not an administrator and is already signed in had no way to say
|
|
8
|
+
* which version they were looking at.
|
|
9
|
+
*
|
|
10
|
+
* ⚠⚠ NO CATALOGUE KEY, on purpose. A product name and a version number need
|
|
11
|
+
* no translation, and a new key would drop every language pack below 100%
|
|
12
|
+
* and put "99% translated" into the README's picture of the Apps list
|
|
13
|
+
* (lesson #417). `filex` is the SOFTWARE's name, not the operator's brand:
|
|
14
|
+
* the line answers "which filex is this", which a renamed instance is too.
|
|
15
|
+
*
|
|
16
|
+
* ⚠ The SERVER's own string (`GET /api/files/capabilities` → `version`),
|
|
17
|
+
* kept as it is: a development build says `0.1.0-dev`, and that is the
|
|
18
|
+
* honest answer. The one value it will not print is the client's own
|
|
19
|
+
* placeholder, `0.0.0` — the capabilities store holds it until the real
|
|
20
|
+
* answer lands, and a menu opened in that moment would otherwise announce a
|
|
21
|
+
* version that has never existed. Unknown is said by saying nothing.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** The software's name, as every version line spells it. */
|
|
25
|
+
export const PRODUCT_NAME = 'filex';
|
|
26
|
+
|
|
27
|
+
/** The client's "not known yet" — never a real server's answer. */
|
|
28
|
+
const PLACEHOLDER = '0.0.0';
|
|
29
|
+
|
|
30
|
+
/** `filex 0.43.0`, or '' while the version is not known. */
|
|
31
|
+
export function productVersionLine(version: string | null | undefined): string {
|
|
32
|
+
const v = String(version ?? '').trim();
|
|
33
|
+
if (!v || v === PLACEHOLDER) return '';
|
|
34
|
+
return `${PRODUCT_NAME} ${v}`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much room an outward-facing body needs.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ The public page has ONE look and three widths — the three the Go-rendered
|
|
5
|
+
* pages had until v0.42.2 (`.card` 400px centred, the drop card 520px, and
|
|
6
|
+
* `.card--folder` 880px). Which one a link gets is decided HERE, from the
|
|
7
|
+
* kind the server answered with, so the page that renders a link and the
|
|
8
|
+
* preview an operator is shown on the Corporate identity page cannot come to
|
|
9
|
+
* different conclusions — and so that no body carries a layout of its own
|
|
10
|
+
* (the whole reason the three pages drifted apart the first time).
|
|
11
|
+
*
|
|
12
|
+
* ⚠ Every state that is not `ready` is drawn as a `gate` whatever this says:
|
|
13
|
+
* a PIN box, "this link is not available" and "thank you" are short, and the
|
|
14
|
+
* shell forces it (`PublicShell.layout`).
|
|
15
|
+
*/
|
|
16
|
+
export type PublicLayout = 'gate' | 'form' | 'wide';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `app` gets the wide card (a wizard with a document in it) and so does a
|
|
20
|
+
* folder that has a LISTING to show; a file request gets the middle one,
|
|
21
|
+
* because it is a form with labels; a single document — and anything
|
|
22
|
+
* unrecognised — gets the narrow centred card, which is the one the
|
|
23
|
+
* reference screen is.
|
|
24
|
+
*
|
|
25
|
+
* ⚠ `list` is why a folder is not simply wide. The server does not send a
|
|
26
|
+
* listing for every folder share yet, and an 880px card holding a heading
|
|
27
|
+
* and two buttons is a worse page than a 520px one holding the same thing.
|
|
28
|
+
* It widens the moment entries arrive, which is what the old `.card--folder`
|
|
29
|
+
* was for.
|
|
30
|
+
*/
|
|
31
|
+
export function publicLayoutFor(
|
|
32
|
+
kind: string | null | undefined,
|
|
33
|
+
opts: { list?: boolean } = {},
|
|
34
|
+
): PublicLayout {
|
|
35
|
+
if (kind === 'app') return 'wide';
|
|
36
|
+
if (kind === 'folder') return opts.list ? 'wide' : 'form';
|
|
37
|
+
if (kind === 'drop') return 'form';
|
|
38
|
+
return 'gate';
|
|
39
|
+
}
|