@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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* appLock — an app plugin's hold on a file, as words and as a refusal.
|
|
3
|
+
*
|
|
4
|
+
* A signing app freezes the document while its signers work on it
|
|
5
|
+
* (`files:lock`, docs/APP-PLUGINS-API.md → "File locks"). Two things reach
|
|
6
|
+
* the browser from that:
|
|
7
|
+
*
|
|
8
|
+
* • the LISTING says so — `locked: true` + `lock: {plugin, reason, until}`
|
|
9
|
+
* on the row, with `perm` already capped at `viewer` for everyone, so
|
|
10
|
+
* the menu hides the write verbs without knowing anything about locks;
|
|
11
|
+
* • the SERVER says so — rename / move / delete of the file, or of any
|
|
12
|
+
* folder above it, answer `423 {"error":"locked", plugin, path, reason,
|
|
13
|
+
* until}`.
|
|
14
|
+
*
|
|
15
|
+
* ⚠⚠ The 423 is the whole reason this file exists. Without it the refusal
|
|
16
|
+
* falls into the generic status map as "Error (423)" — or, worse, gets read
|
|
17
|
+
* as a permission problem and told to the person as "you are not allowed to
|
|
18
|
+
* do this". They ARE allowed; a named app is holding the file until a named
|
|
19
|
+
* date, and that sentence is the only one they can act on. So: one parser,
|
|
20
|
+
* one phrasing, read by every surface that can hit the refusal.
|
|
21
|
+
*
|
|
22
|
+
* ⚠ ONE reader for both facts. The badge on the row, the line in the details
|
|
23
|
+
* panel and the toast after a refusal all take their words from `lockWords`
|
|
24
|
+
* — a second phrasing is how the badge says "locked by sign" while the toast
|
|
25
|
+
* says "permission denied" about the same file.
|
|
26
|
+
*/
|
|
27
|
+
import type { FileNode } from '../types/FileNode';
|
|
28
|
+
import type { AppLock } from '../types/Plugins';
|
|
29
|
+
import { labelOf } from './pluginLabel';
|
|
30
|
+
|
|
31
|
+
export type { AppLock };
|
|
32
|
+
|
|
33
|
+
/** The lock a listing row carries, or `null`. */
|
|
34
|
+
export function lockOf(node: Pick<FileNode, 'locked' | 'lock'> | null | undefined): AppLock | null {
|
|
35
|
+
if (!node || node.locked !== true) return null;
|
|
36
|
+
const l = node.lock;
|
|
37
|
+
if (!l || typeof l !== 'object' || typeof l.plugin !== 'string' || !l.plugin) return null;
|
|
38
|
+
return l;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Is any row of this selection frozen by an app? */
|
|
42
|
+
export function anyLocked(nodes: readonly Pick<FileNode, 'locked' | 'lock'>[]): boolean {
|
|
43
|
+
return nodes.some((n) => lockOf(n) !== null);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The body of a `423 {"error": "locked", …}`. */
|
|
47
|
+
export interface LockedRefusal extends AppLock {
|
|
48
|
+
/** The path the server refused — the file, or the folder above it. */
|
|
49
|
+
path?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A rejected request that was a LOCK, not a permission problem — `null` for
|
|
54
|
+
* anything else, so a caller can keep its existing error path untouched.
|
|
55
|
+
*
|
|
56
|
+
* ⚠ The status alone is the test that matters (`useFileApi` puts the raw body
|
|
57
|
+
* in `.detail` and never parses it). A body that is missing or unparseable
|
|
58
|
+
* still produces a refusal, with an empty plugin name: "an app is holding
|
|
59
|
+
* this file" beats "Error (423)" even when the server said no more than that.
|
|
60
|
+
*/
|
|
61
|
+
export function lockedRefusal(err: unknown): LockedRefusal | null {
|
|
62
|
+
const e = err as { status?: number; detail?: string } | null;
|
|
63
|
+
if (!e || e.status !== 423) return null;
|
|
64
|
+
const out: LockedRefusal = { plugin: '' };
|
|
65
|
+
try {
|
|
66
|
+
const body = JSON.parse(String(e.detail ?? '')) as Record<string, unknown>;
|
|
67
|
+
if (typeof body.plugin === 'string') out.plugin = body.plugin;
|
|
68
|
+
if (body.plugin_label && typeof body.plugin_label === 'object') {
|
|
69
|
+
out.plugin_label = body.plugin_label as AppLock['plugin_label'];
|
|
70
|
+
}
|
|
71
|
+
if (typeof body.path === 'string') out.path = body.path;
|
|
72
|
+
if (typeof body.reason === 'string' && body.reason) out.reason = body.reason;
|
|
73
|
+
if (body.reason_text && typeof body.reason_text === 'object') out.reason_text = body.reason_text as AppLock['reason_text'];
|
|
74
|
+
if (typeof body.until === 'string' && body.until) out.until = body.until;
|
|
75
|
+
} catch {
|
|
76
|
+
/* an unreadable body is still a lock — see the note above */
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** What `lockWords` needs from the caller's locale. */
|
|
82
|
+
export interface LockWordsHost {
|
|
83
|
+
t: (key: string, vars?: Record<string, string | number>) => string;
|
|
84
|
+
/** The explorer's own date renderer, so a lock reads on the user's clock. */
|
|
85
|
+
formatDate?: (ms: number | undefined | null, opts?: { time?: boolean }) => string;
|
|
86
|
+
/** The reader's language: which of the reason's languages to show. */
|
|
87
|
+
locale?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A lock's reason in the reader's language: the app's own words for it
|
|
92
|
+
* (`reason_text`, a manifest message the server kept in every language the
|
|
93
|
+
* app wrote), else the plain `reason`.
|
|
94
|
+
*
|
|
95
|
+
* ⚠ The reason used to be ONE string in the language of whoever took the
|
|
96
|
+
* lock, so a German administrator read the Turkish requester's "imzalar
|
|
97
|
+
* toplanıyor" (v0.43.0 wave 2).
|
|
98
|
+
*/
|
|
99
|
+
export function lockReasonText(lock: AppLock | null | undefined, locale: string | undefined): string {
|
|
100
|
+
if (!lock) return '';
|
|
101
|
+
return labelOf(lock.reason_text, locale || 'en') || lock.reason || '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* One sentence for a lock: who holds it, why, until when — with every part
|
|
106
|
+
* that is missing simply left out rather than printed as "undefined".
|
|
107
|
+
*/
|
|
108
|
+
export function lockWords(lock: AppLock | LockedRefusal | null, host: LockWordsHost): string {
|
|
109
|
+
if (!lock) return '';
|
|
110
|
+
/* The app's own label first, its manifest name only where the server could
|
|
111
|
+
not resolve one. ⚠ The name is an ADDRESS (`sign`); every other screen
|
|
112
|
+
shows the label ("e-Signature"), and a banner that says "sign locked this
|
|
113
|
+
file" hands the person an identifier they have never seen (v0.43.0). */
|
|
114
|
+
const who = labelOf(lock.plugin_label, host.locale || 'en') || lock.plugin || host.t('applock.some_app');
|
|
115
|
+
const reason = lockReasonText(lock, host.locale);
|
|
116
|
+
const base = reason
|
|
117
|
+
? host.t('applock.held_reason', { app: who, reason })
|
|
118
|
+
: host.t('applock.held', { app: who });
|
|
119
|
+
const until = lockUntilText(lock.until, host);
|
|
120
|
+
return until ? `${base} ${until}` : base;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** " — until 3 Oct 2026", or `''` when the lock has no end. */
|
|
124
|
+
export function lockUntilText(until: string | null | undefined, host: LockWordsHost): string {
|
|
125
|
+
if (!until) return '';
|
|
126
|
+
const ms = Date.parse(until);
|
|
127
|
+
if (!Number.isFinite(ms)) return '';
|
|
128
|
+
const when = host.formatDate ? host.formatDate(ms, { time: true }) : new Date(ms).toISOString();
|
|
129
|
+
return host.t('applock.until', { date: when });
|
|
130
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connection — ONE notice for a connection that is down, however many
|
|
3
|
+
* requests fall over while it is.
|
|
4
|
+
*
|
|
5
|
+
* ⚠⚠ THE STORM THIS EXISTS TO STOP. A page that has lost the server is right
|
|
6
|
+
* to say so, and it used to say so once per failing request: the web app's
|
|
7
|
+
* axios interceptor raised `errors.network` as a toast from every refusal
|
|
8
|
+
* with no response. The explorer alone fires several calls a second — the
|
|
9
|
+
* listing, a thumbnail per row, the bell's 15 s poll, the pending-operations
|
|
10
|
+
* poll — so the corner filled with copies of one sentence and the person was
|
|
11
|
+
* left dismissing a stack of them (owner, 2026-09-24: "attığı tüm istekler
|
|
12
|
+
* için ayrı ayrı atıyor o yüzden çok fazla popover çıkıyor").
|
|
13
|
+
*
|
|
14
|
+
* A connection that is down is ONE FACT ABOUT THE PAGE, not one fact per
|
|
15
|
+
* request. So:
|
|
16
|
+
*
|
|
17
|
+
* - a BACKGROUND request — anything the person did not start: a listing, a
|
|
18
|
+
* thumbnail, a poll — is folded. It raises the shared notice and says
|
|
19
|
+
* nothing of its own, however many of them fail;
|
|
20
|
+
* - an ACTION the person is waiting on — an upload, a rename, a save, any
|
|
21
|
+
* write — still reports individually. ⚠ Folding those into a generic
|
|
22
|
+
* "offline" would be worse than the storm: the person is waiting to be
|
|
23
|
+
* told whether the thing they did happened;
|
|
24
|
+
* - the notice CLEARS ITSELF the moment any request gets an answer. Nobody
|
|
25
|
+
* should have to dismiss a stale one.
|
|
26
|
+
*
|
|
27
|
+
* ⚠ No new polling, and no change to any existing cadence. This module never
|
|
28
|
+
* makes a request of its own: it is told what happened by whoever was already
|
|
29
|
+
* talking to the server (the web app's axios interceptor, core's `jsonFetch`
|
|
30
|
+
* and its upload XHR), so recovery is noticed by the next call the page was
|
|
31
|
+
* going to make anyway.
|
|
32
|
+
*
|
|
33
|
+
* ⚠⚠ Shared on purpose — one rule for the web app, the desktop shell and an
|
|
34
|
+
* embedded explorer. A per-surface copy is how two of them come to disagree
|
|
35
|
+
* about what "offline" looks like.
|
|
36
|
+
*
|
|
37
|
+
* ⚠ `navigator.onLine` is deliberately NOT consulted. It answers "this
|
|
38
|
+
* machine has a network", not "filex answers", and it is true for every way
|
|
39
|
+
* this actually happens — the server stopped, the tunnel dropped, the laptop
|
|
40
|
+
* woke on a captive-portal Wi-Fi. Observed failures are the only evidence.
|
|
41
|
+
*/
|
|
42
|
+
import { computed, ref, type ComputedRef } from 'vue';
|
|
43
|
+
|
|
44
|
+
/** Did the person start this request, or did the page? */
|
|
45
|
+
export type RequestKind = 'background' | 'action';
|
|
46
|
+
|
|
47
|
+
/** What the caller should do about a failure it just had. */
|
|
48
|
+
export type FailureVerdict = 'folded' | 'say';
|
|
49
|
+
|
|
50
|
+
const downSince = ref(0);
|
|
51
|
+
const folded = ref(0);
|
|
52
|
+
|
|
53
|
+
/** Is the server unreachable right now, as far as this page has seen? */
|
|
54
|
+
export const connectionDown: ComputedRef<boolean> = computed(() => downSince.value !== 0);
|
|
55
|
+
|
|
56
|
+
/** When it first went (epoch ms), or 0. For a caller that wants to wait. */
|
|
57
|
+
export const connectionDownSince: ComputedRef<number> = computed(() => downSince.value);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* How many background failures the shared notice is standing in for. The
|
|
61
|
+
* notice does not print it — a number that climbs is the storm again, in one
|
|
62
|
+
* line — but a test can assert that many failures made one notice.
|
|
63
|
+
*/
|
|
64
|
+
export const connectionFolded: ComputedRef<number> = computed(() => folded.value);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A request got no answer at all. Raises the shared notice and says whether
|
|
68
|
+
* this particular failure is the caller's to report.
|
|
69
|
+
*
|
|
70
|
+
* ⚠ An ACTION raises the notice too. The connection is down either way, and
|
|
71
|
+
* the person's own message ("could not be renamed") does not explain why.
|
|
72
|
+
*/
|
|
73
|
+
export function noteRequestFailed(kind: RequestKind, now: number = Date.now()): FailureVerdict {
|
|
74
|
+
if (downSince.value === 0) downSince.value = now || 1;
|
|
75
|
+
if (kind === 'background') {
|
|
76
|
+
folded.value += 1;
|
|
77
|
+
return 'folded';
|
|
78
|
+
}
|
|
79
|
+
return 'say';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A request got an answer — any answer. ⚠ Including a 4xx or a 5xx: the
|
|
84
|
+
* server spoke, so the connection is not what is wrong, and a notice that
|
|
85
|
+
* said otherwise would be lying while the person reads a real refusal.
|
|
86
|
+
*/
|
|
87
|
+
export function noteRequestSucceeded(): void {
|
|
88
|
+
if (downSince.value === 0 && folded.value === 0) return;
|
|
89
|
+
downSince.value = 0;
|
|
90
|
+
folded.value = 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Forget everything. For tests, and for a surface being torn down. */
|
|
94
|
+
export function resetConnectionNotice(): void {
|
|
95
|
+
downSince.value = 0;
|
|
96
|
+
folded.value = 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The kind a request of this HTTP method counts as.
|
|
101
|
+
*
|
|
102
|
+
* ⚠ The method, not the URL. A read is the page keeping itself up to date and
|
|
103
|
+
* can be folded; a write is something the person pressed and is waiting on.
|
|
104
|
+
* The one rule both request layers apply, so the web app and an embedded
|
|
105
|
+
* explorer cannot classify the same call differently.
|
|
106
|
+
*/
|
|
107
|
+
export function kindOfMethod(method: string | undefined): RequestKind {
|
|
108
|
+
const m = String(method ?? 'get').toLowerCase();
|
|
109
|
+
return m === 'get' || m === 'head' || m === 'options' ? 'background' : 'action';
|
|
110
|
+
}
|
|
@@ -157,14 +157,19 @@ export function blockedReason(
|
|
|
157
157
|
|
|
158
158
|
/** One row in the picker's list. */
|
|
159
159
|
export interface DestinationRow {
|
|
160
|
-
/** Wire path of the folder. */
|
|
160
|
+
/** Wire path of the folder (or, in a file pick, the file). */
|
|
161
161
|
path: string;
|
|
162
|
-
/** What to show — the
|
|
162
|
+
/** What to show — the entry's own name. */
|
|
163
163
|
label: string;
|
|
164
164
|
/** May the caller write into it? Drives the disabled Choose button. */
|
|
165
165
|
writable: boolean;
|
|
166
166
|
/** Set when this row is one of the folders being moved, or inside one. */
|
|
167
167
|
blocked: 'self' | 'descendant' | null;
|
|
168
|
+
/**
|
|
169
|
+
* Folders are walked into; a file (offered only when the picker is asked
|
|
170
|
+
* for one — an app plugin's `file-chooser`) is the answer itself.
|
|
171
|
+
*/
|
|
172
|
+
kind: 'dir' | 'file';
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
/**
|
|
@@ -184,15 +189,25 @@ export interface DestinationRow {
|
|
|
184
189
|
export function destinationRows(
|
|
185
190
|
files: FileNode[] | undefined,
|
|
186
191
|
moving?: string[],
|
|
192
|
+
opts: { files?: boolean } = {},
|
|
187
193
|
): DestinationRow[] {
|
|
188
194
|
const out: DestinationRow[] = [];
|
|
189
195
|
for (const f of files ?? []) {
|
|
190
|
-
if (f.type !== 'dir')
|
|
196
|
+
if (f.type !== 'dir') {
|
|
197
|
+
// A file row is a choice, not a place: it needs no write check (the
|
|
198
|
+
// chooser READS it) and cannot be "inside" a moving folder in any way
|
|
199
|
+
// that matters, so it carries neither.
|
|
200
|
+
if (opts.files && f.type === 'file') {
|
|
201
|
+
out.push({ path: f.path, label: f.basename, writable: true, blocked: null, kind: 'file' });
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
191
205
|
out.push({
|
|
192
206
|
path: f.path,
|
|
193
207
|
label: f.basename,
|
|
194
208
|
writable: permAllowsWrite(f.perm as string | undefined),
|
|
195
209
|
blocked: blockedReason(f.path, moving),
|
|
210
|
+
kind: 'dir',
|
|
196
211
|
});
|
|
197
212
|
}
|
|
198
213
|
return out;
|
|
@@ -211,6 +226,7 @@ export function driveRows(storages: string[] | undefined, moving?: string[]): De
|
|
|
211
226
|
// order — the alternative greys out every drive until each is opened.
|
|
212
227
|
writable: true,
|
|
213
228
|
blocked: blockedReason(path, moving),
|
|
229
|
+
kind: 'dir',
|
|
214
230
|
};
|
|
215
231
|
});
|
|
216
232
|
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* direction — which way a language is written, and where that answer goes.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ ONE LIST. Whether a language is written right to left is decided on the
|
|
5
|
+
* SERVER, by `wire.IsRTL` (backend/pkg/pluginkit/wire/langpack.go — the
|
|
6
|
+
* primary-language list plus the script subtags), and it reaches the browser
|
|
7
|
+
* as the `rtl` flag on each row of the offered-language list
|
|
8
|
+
* (`/api/public/branding` → `ui_locales`, folded in by `lib/uiLocales`).
|
|
9
|
+
* This file only READS that flag. It must never grow a table of its own:
|
|
10
|
+
* `Intl.Locale#getTextInfo()` looks like a shortcut and is one — to a second
|
|
11
|
+
* list (the browser's CLDR copy) that Firefox does not ship at all and that
|
|
12
|
+
* would disagree with the install review about the same pack.
|
|
13
|
+
*
|
|
14
|
+
* The two catalogues the product ships (English, Turkish) are left to right,
|
|
15
|
+
* and a code nobody offers is left to right: a language whose words are not
|
|
16
|
+
* on screen does not get to turn the screen around.
|
|
17
|
+
*
|
|
18
|
+
* ⚠⚠ THE RULE, for every surface: `dir` follows the language whose words the
|
|
19
|
+
* surface is showing.
|
|
20
|
+
*
|
|
21
|
+
* - A whole page (the admin panel, the drive, the public share/PIN/drop
|
|
22
|
+
* pages, the signing wizard) carries it on `<html>`, DERIVED from
|
|
23
|
+
* `<html lang>` by `syncDocumentDir` — every place that already sets the
|
|
24
|
+
* page's language (web/src/i18n, PublicLinkPage) sets its direction with
|
|
25
|
+
* it, without having to know that direction exists.
|
|
26
|
+
* - An embedded explorer carries it on its own root, from ITS locale — not
|
|
27
|
+
* from the host page. An Arabic explorer inside an English page reads
|
|
28
|
+
* right to left; an English explorer inside an Arabic page reads left to
|
|
29
|
+
* right. Laying English out mirrored because the page around it is Arabic
|
|
30
|
+
* (or the reverse) scrambles exactly the words the component exists to
|
|
31
|
+
* show, and the host already chose the component's language by setting
|
|
32
|
+
* `locale`.
|
|
33
|
+
* - Anything Teleported out of that root (menus, popovers, the tour, the
|
|
34
|
+
* quick-look hint) carries `dir` again, because `<body>` is not inside the
|
|
35
|
+
* explorer and would hand it the HOST's direction. `useLocale().dir` is the
|
|
36
|
+
* value to bind.
|
|
37
|
+
*
|
|
38
|
+
* ⚠⚠ Direction is about the INTERFACE. Document space — a PDF page, an image,
|
|
39
|
+
* a drawing, a crop box — has its own coordinates and never mirrors (see
|
|
40
|
+
* lib/pdfFieldsGeom). Nothing here may be applied to those.
|
|
41
|
+
*/
|
|
42
|
+
import { computed, watch } from 'vue';
|
|
43
|
+
import { availableLocales, localesVersion, normalizeLocaleCode } from './uiLocales';
|
|
44
|
+
|
|
45
|
+
export type TextDirection = 'ltr' | 'rtl';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The offered codes the server flagged right to left.
|
|
49
|
+
*
|
|
50
|
+
* ⚠ Read through a widened row type: the flag is added to
|
|
51
|
+
* `PublicLocaleOption` by the language-pack platform, and a row that does not
|
|
52
|
+
* carry it (an older server, the two built-ins) is simply left to right.
|
|
53
|
+
* `localesVersion` is read so the set is re-derived the moment the list — and
|
|
54
|
+
* with it a pack's flag — arrives or changes.
|
|
55
|
+
*/
|
|
56
|
+
const rtlCodes = computed(() => {
|
|
57
|
+
void localesVersion.value;
|
|
58
|
+
const out = new Set<string>();
|
|
59
|
+
for (const row of availableLocales() as Array<{ code: string; rtl?: boolean }>) {
|
|
60
|
+
if (row.rtl === true) out.add(row.code);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/** `'rtl'` when the server says this language is written right to left. */
|
|
66
|
+
export function localeDir(code: string | null | undefined): TextDirection {
|
|
67
|
+
const c = normalizeLocaleCode(code ?? '');
|
|
68
|
+
return c && rtlCodes.value.has(c) ? 'rtl' : 'ltr';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Keep `root`'s `dir` derived from its `lang`, for as long as the page lives.
|
|
73
|
+
*
|
|
74
|
+
* ⚠ An observer on `lang`, not a call from each place that sets the language:
|
|
75
|
+
* there are several (the admin panel's language decision, the public link
|
|
76
|
+
* page, and whatever sets it next), and a direction that each of them had to
|
|
77
|
+
* remember to set is a direction one of them forgets. The observer's callback
|
|
78
|
+
* runs as a microtask — before the next paint — so the words and the layout
|
|
79
|
+
* turn together.
|
|
80
|
+
*
|
|
81
|
+
* ⚠ Also re-applied when the language LIST changes: a pack's language can be
|
|
82
|
+
* on `<html lang>` before the list saying it is right to left has arrived
|
|
83
|
+
* (the admin panel holds a stored choice while the list is in flight).
|
|
84
|
+
*
|
|
85
|
+
* Only for a host that owns the whole page. An embed never calls this — the
|
|
86
|
+
* host page's direction is the host's business (see the rule above).
|
|
87
|
+
*
|
|
88
|
+
* @returns a stop function (tests, hot reload).
|
|
89
|
+
*/
|
|
90
|
+
export function syncDocumentDir(
|
|
91
|
+
root: HTMLElement | null = typeof document !== 'undefined' ? document.documentElement : null,
|
|
92
|
+
): () => void {
|
|
93
|
+
if (!root) return () => {};
|
|
94
|
+
const apply = () => {
|
|
95
|
+
const want = localeDir(root.getAttribute('lang'));
|
|
96
|
+
if (root.getAttribute('dir') !== want) root.setAttribute('dir', want);
|
|
97
|
+
};
|
|
98
|
+
apply();
|
|
99
|
+
const mo = typeof MutationObserver !== 'undefined' ? new MutationObserver(apply) : null;
|
|
100
|
+
mo?.observe(root, { attributes: true, attributeFilter: ['lang'] });
|
|
101
|
+
const stop = watch(localesVersion, apply);
|
|
102
|
+
return () => {
|
|
103
|
+
mo?.disconnect();
|
|
104
|
+
stop();
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ── interaction geometry ─────────────────────────────────────────────── */
|
|
109
|
+
/*
|
|
110
|
+
* ⚠⚠ Pointer events and `getBoundingClientRect()` are PHYSICAL: x grows to the
|
|
111
|
+
* right whatever the page's direction. Every gesture that means "toward the
|
|
112
|
+
* end of the line" — widening a column by its trailing edge, moving a column
|
|
113
|
+
* later, opening a menu beside its anchor — has to translate between the two.
|
|
114
|
+
* These helpers are that translation, in one place; a component that does its
|
|
115
|
+
* own `dir === 'rtl' ? … : …` arithmetic is a component that will get one of
|
|
116
|
+
* the four cases wrong.
|
|
117
|
+
*
|
|
118
|
+
* ⚠⚠ Never for DOCUMENT space (a PDF page, an image, a crop): a page's x is
|
|
119
|
+
* the page's own and is not mirrored (lib/pdfFieldsGeom).
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The direction an element is actually laid out in: its nearest explicit
|
|
124
|
+
* `dir="ltr|rtl"`. `dir="auto"` (user text isolated with `<bdi>`-like
|
|
125
|
+
* behaviour) is skipped — it says nothing about the layout around it.
|
|
126
|
+
*/
|
|
127
|
+
export function dirOfElement(el: Element | null | undefined): TextDirection {
|
|
128
|
+
const host = el?.closest?.('[dir="rtl"], [dir="ltr"]');
|
|
129
|
+
return host?.getAttribute('dir') === 'rtl' ? 'rtl' : 'ltr';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** +1 for the physical x axis in LTR, -1 in RTL: "toward the inline end". */
|
|
133
|
+
export function inlineSign(dir: TextDirection): 1 | -1 {
|
|
134
|
+
return dir === 'rtl' ? -1 : 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A horizontal arrow key as a step along the line: +1 = toward the inline END
|
|
139
|
+
* (→ in LTR, ← in RTL), -1 = toward the start, 0 = not a horizontal arrow.
|
|
140
|
+
* The key a person presses moves things the way the arrow on it points.
|
|
141
|
+
*/
|
|
142
|
+
export function inlineKeyStep(key: string, dir: TextDirection): -1 | 0 | 1 {
|
|
143
|
+
const physical = key === 'ArrowRight' ? 1 : key === 'ArrowLeft' ? -1 : 0;
|
|
144
|
+
if (physical === 0) return 0;
|
|
145
|
+
return (physical * inlineSign(dir)) as -1 | 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The x of a box's inline START edge — its left in LTR, its right in RTL. */
|
|
149
|
+
export function inlineStartX(r: { left: number; right: number }, dir: TextDirection): number {
|
|
150
|
+
return dir === 'rtl' ? r.right : r.left;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The x of a box's inline END edge — its right in LTR, its left in RTL. */
|
|
154
|
+
export function inlineEndX(r: { left: number; right: number }, dir: TextDirection): number {
|
|
155
|
+
return dir === 'rtl' ? r.left : r.right;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The physical `left` of a floating box opened at `anchor` (a viewport x):
|
|
160
|
+
* it opens toward the inline END — down-right of the pointer in LTR,
|
|
161
|
+
* down-LEFT in RTL — flips to the other side of the anchor when that would
|
|
162
|
+
* overflow the viewport, and clamps when neither side fits.
|
|
163
|
+
*
|
|
164
|
+
* ⚠ Written ONCE, as the LTR rule, and mirrored by mapping x → viewport − x:
|
|
165
|
+
* the RTL behaviour is the LTR behaviour seen in a mirror by construction,
|
|
166
|
+
* and the LTR answer is byte-for-byte what the menus did before RTL existed.
|
|
167
|
+
*/
|
|
168
|
+
export function openAlongInline(
|
|
169
|
+
anchor: number,
|
|
170
|
+
size: number,
|
|
171
|
+
viewport: number,
|
|
172
|
+
dir: TextDirection,
|
|
173
|
+
margin = 8,
|
|
174
|
+
): number {
|
|
175
|
+
const a = dir === 'rtl' ? viewport - anchor : anchor;
|
|
176
|
+
let left = a;
|
|
177
|
+
if (a + size > viewport - margin) {
|
|
178
|
+
const flipped = a - size;
|
|
179
|
+
left = flipped >= margin ? flipped : Math.max(margin, viewport - size - margin);
|
|
180
|
+
}
|
|
181
|
+
return dir === 'rtl' ? viewport - left - size : left;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Like `openAlongInline`, without the flip: the box starts at `anchor` and is
|
|
186
|
+
* only pushed back inside the viewport (popovers that hang from a chip and
|
|
187
|
+
* must stay beside it). Same mirror construction.
|
|
188
|
+
*/
|
|
189
|
+
export function clampAlongInline(
|
|
190
|
+
anchor: number,
|
|
191
|
+
size: number,
|
|
192
|
+
viewport: number,
|
|
193
|
+
dir: TextDirection,
|
|
194
|
+
margin = 8,
|
|
195
|
+
): number {
|
|
196
|
+
const a = dir === 'rtl' ? viewport - anchor : anchor;
|
|
197
|
+
const left = a + size > viewport - margin ? Math.max(margin, viewport - margin - size) : a;
|
|
198
|
+
return dir === 'rtl' ? viewport - left - size : left;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ── text that must read left to right inside right-to-left text ───────── */
|
|
202
|
+
|
|
203
|
+
const LRI = '\u2066';
|
|
204
|
+
const RLI = '\u2067';
|
|
205
|
+
const FSI = '\u2068';
|
|
206
|
+
const PDI = '\u2069';
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The runs of a right-to-left sentence that are MACHINE text and read left to
|
|
210
|
+
* right whatever surrounds them:
|
|
211
|
+
*
|
|
212
|
+
* - a number pair — `3 / 10`, `1.2 MB / 5 GB`. Spaced out, the slash is a
|
|
213
|
+
* neutral between two numbers and the bidi algorithm resolves it to the
|
|
214
|
+
* paragraph's direction, so an RTL line draws `10 / 3`;
|
|
215
|
+
* - a `word:value` token — the search syntax `tag:…`, a URL
|
|
216
|
+
* (`https://…`), a Windows path (`C:\Users\…`), the token syntax a
|
|
217
|
+
* refusal names (`root:<storage>://<folder>`). Its colon sits between
|
|
218
|
+
* Latin and Arabic letters and lands on the wrong side: `tag:وسم` draws
|
|
219
|
+
* as `وسم:tag`, and `root:<storage>://<folder>` loses its closing `>` to
|
|
220
|
+
* the far left of the run, mirrored into `<`;
|
|
221
|
+
* - an absolute PATH — `/var/lib/filex/report.pdf`. Its LEADING slash is a
|
|
222
|
+
* neutral between the Arabic before it and the Latin after it, so it
|
|
223
|
+
* takes the paragraph's direction and is drawn at the RIGHT-hand END of
|
|
224
|
+
* the run: the line reads `var/lib/filex/report.pdf/`. Measured in
|
|
225
|
+
* Chromium at `dir="rtl"` (v0.43.0): two rectangles, the second of them
|
|
226
|
+
* the six pixels of that slash, against one when the run is isolated.
|
|
227
|
+
* ⚠ ONE segment is a path too — `/informe.pdf`, a file at the root of
|
|
228
|
+
* a storage, which a notification body names that way. It was left out
|
|
229
|
+
* (the rule wanted two), and the admin Notifications page drew it as
|
|
230
|
+
* `informe.pdf/` in Arabic (v0.43.0 pack agent). A single segment is
|
|
231
|
+
* isolated only when its slash STARTS something: not after a Latin
|
|
232
|
+
* letter or digit (`and/or`, `km/h`, `24/09`, `TCP/IP`), not after
|
|
233
|
+
* another slash, dot or colon, and only when the segment holds a Latin
|
|
234
|
+
* letter or digit — so a lone `/` between two Arabic words, `و/أو`,
|
|
235
|
+
* and a spaced `3 / 10` (the rule above answers that one) stay as
|
|
236
|
+
* they are. A trailing full stop is the sentence's, not the path's.
|
|
237
|
+
* ⚠ And the segment has to END there — no colon after it. In
|
|
238
|
+
* `/المستندات/tag:2026/…` the slash before `tag` is preceded by an
|
|
239
|
+
* Arabic letter and so starts a one-segment path; left unchecked it
|
|
240
|
+
* took `/tag`, and the `word:value` token after it — the run that
|
|
241
|
+
* actually needs isolating — was cut in two.
|
|
242
|
+
*
|
|
243
|
+
* Each is wrapped in LEFT-TO-RIGHT ISOLATE … POP DIRECTIONAL ISOLATE
|
|
244
|
+
* (U+2066 … U+2069), which works where markup cannot go — an input's
|
|
245
|
+
* placeholder, a `title`, an `aria-label` — and is invisible everywhere.
|
|
246
|
+
* A number that `isolateValue` already wrapped still counts: the pattern
|
|
247
|
+
* steps over an isolate mark on either side of it.
|
|
248
|
+
*
|
|
249
|
+
* ⚠ Called ONLY for a right-to-left language — never on its own. `foreignText`
|
|
250
|
+
* below is the one entry point that knows which languages those are; in LTR
|
|
251
|
+
* the text comes back untouched, so the English and Turkish strings are
|
|
252
|
+
* byte-for-byte what they were.
|
|
253
|
+
*
|
|
254
|
+
* ⚠⚠ IDEMPOTENT. Text now reaches a screen through more than one of these
|
|
255
|
+
* gates (a catalogue sentence through `useLocale().t`, a server's sentence
|
|
256
|
+
* through `foreignText`, a failure said by lib/errorWords, a notification
|
|
257
|
+
* composed by the reader), and a run that is already isolated must not
|
|
258
|
+
* collect a second pair of marks — nested isolates draw the same but make a
|
|
259
|
+
* string that no longer equals the one a test wrote.
|
|
260
|
+
*/
|
|
261
|
+
const LTR_RUNS =
|
|
262
|
+
/[\u2066-\u2069]?\d[\d.,]*(?:\s?[A-Za-z%]{1,3})?[\u2066-\u2069]?\s*\/\s*[\u2066-\u2069]?\d[\d.,]*(?:\s?[A-Za-z%]{1,3})?[\u2066-\u2069]?|[A-Za-z][\w+.-]*:[^\s,\u060C)\u2066-\u2069]+|\/(?:[A-Za-z0-9_.~%+-]+\/)+[A-Za-z0-9_.~%+-]*|(?<![A-Za-z0-9_.~%+:\/-])\/(?=[A-Za-z0-9_.~%+-]*[A-Za-z0-9])[A-Za-z0-9_.~%+-]*[A-Za-z0-9_~%+-](?!\.?[A-Za-z0-9_~%+:-])/g;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Is `s` already ONE isolate, opened at its first character and closed at its
|
|
266
|
+
* last?
|
|
267
|
+
*
|
|
268
|
+
* ⚠ Not "starts with an initiator and ends with a terminator": the number-pair
|
|
269
|
+
* rule above matches a mark on either side of each number, so
|
|
270
|
+
* `FSI 3 PDI / FSI 10 PDI` — two values isolated separately by `isolateValue` — looks like
|
|
271
|
+
* that and is NOT wrapped as a pair. The depth has to stay inside until the
|
|
272
|
+
* end, or the two halves would never be held together and the slash would
|
|
273
|
+
* still resolve to the paragraph.
|
|
274
|
+
*/
|
|
275
|
+
function wrapped(s: string): boolean {
|
|
276
|
+
if (!(s[0] === LRI || s[0] === FSI || s[0] === RLI) || s[s.length - 1] !== PDI) return false;
|
|
277
|
+
let depth = 0;
|
|
278
|
+
for (let i = 0; i < s.length; i++) {
|
|
279
|
+
const c = s[i];
|
|
280
|
+
if (c === LRI || c === FSI || c === RLI) depth += 1;
|
|
281
|
+
else if (c === PDI) depth -= 1;
|
|
282
|
+
if (depth === 0 && i < s.length - 1) return false;
|
|
283
|
+
}
|
|
284
|
+
return depth === 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function isolateLtrRuns(text: string): string {
|
|
288
|
+
return text.replace(LTR_RUNS, (m: string, offset: number, whole: string) => {
|
|
289
|
+
if (wrapped(m)) return m;
|
|
290
|
+
const before = whole[offset - 1];
|
|
291
|
+
const after = whole[offset + m.length];
|
|
292
|
+
if ((before === LRI || before === FSI) && after === PDI) return m;
|
|
293
|
+
return LRI + m + PDI;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* ⚠⚠ TEXT FILEX DID NOT WRITE, made readable in `code`'s direction.
|
|
299
|
+
*
|
|
300
|
+
* A sentence the SERVER composed (`server.*`, reaching the browser as the
|
|
301
|
+
* `message` of a refusal), an installed app's own words, a notification built
|
|
302
|
+
* from what happened — none of it passes through `useLocale().t` or the admin
|
|
303
|
+
* panel's post-translation hook, so none of it was isolated. It is exactly
|
|
304
|
+
* the text that carries machine runs: a path, a URL, a command, a token
|
|
305
|
+
* syntax.
|
|
306
|
+
*
|
|
307
|
+
* Measured in the Arabic admin panel (v0.43.0 pack round, `G:/filex-lang-ar`):
|
|
308
|
+
* `server.token.scope_unknown` names the syntax `root:<storage>://<folder>`,
|
|
309
|
+
* and the closing `>` — a neutral between the Latin run and the Arabic after
|
|
310
|
+
* it — resolved to the paragraph's direction, jumped to the FAR LEFT of the
|
|
311
|
+
* run and mirrored into `<`, so the line read `…<root:<storage>://<folder`.
|
|
312
|
+
* The same sentence drawn from the catalogue was correct, which is what makes
|
|
313
|
+
* this read as a broken pack rather than as a missing call.
|
|
314
|
+
*
|
|
315
|
+
* ⚠ A pack cannot fix it: bidi control characters are forbidden in a
|
|
316
|
+
* translation and the pack's own validator refuses them (`BIDI`). It is the
|
|
317
|
+
* render site's job, and this is the render site's one function.
|
|
318
|
+
*/
|
|
319
|
+
export function foreignText(code: string | null | undefined, text: string): string {
|
|
320
|
+
return localeDir(code) === 'rtl' ? isolateLtrRuns(text) : text;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* A value interpolated into a sentence — a file or folder name, a person —
|
|
325
|
+
* wrapped in FIRST-STRONG ISOLATE … POP DIRECTIONAL ISOLATE, so its own
|
|
326
|
+
* letters decide its direction and it cannot drag the sentence around it:
|
|
327
|
+
* `2026 report.pdf` inside an Arabic sentence keeps its number in front, and
|
|
328
|
+
* an Arabic name inside a sentence keeps its own order.
|
|
329
|
+
*
|
|
330
|
+
* ⚠ RTL only (`useLocale().t`), and for DISPLAY: the marks are invisible but
|
|
331
|
+
* real characters, so a string that becomes data — a file name, a command to
|
|
332
|
+
* copy — must never come from a right-to-left `t()` with a value in it.
|
|
333
|
+
*/
|
|
334
|
+
export function isolateValue(v: string): string {
|
|
335
|
+
return FSI + v + PDI;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The `dir` for a piece of the person's CONTENT drawn inside the interface —
|
|
340
|
+
* a file's first lines on its card. ⚠ Content is not interface: an English
|
|
341
|
+
* README previewed in an Arabic explorer still reads left to right, and an
|
|
342
|
+
* Arabic note in an English one right to left.
|
|
343
|
+
*
|
|
344
|
+
* code → `ltr`: machine text, whatever the language around it;
|
|
345
|
+
* text → `auto`: the first letter of the text decides (a heading's `#`
|
|
346
|
+
* stays in front instead of trailing an RTL line);
|
|
347
|
+
* table → none: a table follows the interface, like every other table.
|
|
348
|
+
*/
|
|
349
|
+
export function contentDir(kind: string): 'ltr' | 'auto' | undefined {
|
|
350
|
+
if (kind === 'code') return 'ltr';
|
|
351
|
+
if (kind === 'text') return 'auto';
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
package/src/lib/dragGhost.ts
CHANGED
|
@@ -19,6 +19,10 @@ export function applyDragGhost(ev: DragEvent, name: string, count: number): void
|
|
|
19
19
|
el.className = 'fe-dragghost';
|
|
20
20
|
const label = document.createElement('span');
|
|
21
21
|
label.className = 'fe-dragghost__name';
|
|
22
|
+
// ⚠ RTL: a file name is the person's text, not the interface's — `auto`
|
|
23
|
+
// lets `Report (final).pdf` read right inside an Arabic page and an Arabic
|
|
24
|
+
// name read right inside an English one.
|
|
25
|
+
label.dir = 'auto';
|
|
22
26
|
label.textContent = name;
|
|
23
27
|
el.appendChild(label);
|
|
24
28
|
if (count > 1) {
|
|
@@ -29,6 +33,8 @@ export function applyDragGhost(ev: DragEvent, name: string, count: number): void
|
|
|
29
33
|
}
|
|
30
34
|
el.style.position = 'fixed';
|
|
31
35
|
el.style.top = '-1000px';
|
|
36
|
+
// Physical on purpose: FIXED boxes add no scrollable overflow, so off the
|
|
37
|
+
// left edge is off screen in either direction (the RTL guard allow-lists it).
|
|
32
38
|
el.style.left = '-1000px';
|
|
33
39
|
document.body.appendChild(el);
|
|
34
40
|
dt.setDragImage(el, 14, 14);
|