@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.
- 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-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-DS3mb3k8.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-CIrOn63I.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 +5241 -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 +1207 -343
- 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 +47 -17
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -56
- package/src/components/SSHKeysPanel.vue +79 -26
- 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 -27
- 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 +121 -0
- 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 +522 -119
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +539 -155
- 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 +2895 -241
- 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 +58 -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-CLNevfPE.js +0 -124
- package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
- package/dist/CsvViewer-K8XA3z7N.js +0 -141
- package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
- package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
- package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
- package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
- package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
- package/dist/MermaidViewer-DqucH_io.js +0 -137
- package/dist/MermaidViewer-DqucH_io.js.map +0 -1
- package/dist/PsdViewer-CwiVENmq.js +0 -122
- package/dist/PsdViewer-CwiVENmq.js.map +0 -1
- package/dist/TiffViewer-CpewSVmQ.js +0 -142
- package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
- package/dist/Viewer3D-CIrOn63I.js.map +0 -1
- package/dist/index-Dx0IbBcI.js +0 -26831
- package/dist/index-Dx0IbBcI.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surfaces — what a visitor with a link, and no account, is told.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ ONE shell draws all of them (`components/public/PublicShell.vue`): a
|
|
5
|
+
* share (`/s/<token>`), a file request (`/d/<token>`) and an app plugin's
|
|
6
|
+
* page (a share whose `kind` is `app`). Before v3 the first two were HTML
|
|
7
|
+
* that Go wrote by hand and the third was a Vue screen, so the PIN box of a
|
|
8
|
+
* signature request looked nothing like the PIN box of a download and
|
|
9
|
+
* neither carried the instance's own colours. The shapes below are the
|
|
10
|
+
* seam: everything the shell needs to draw ANY of the three, and nothing
|
|
11
|
+
* that depends on which one it is.
|
|
12
|
+
*
|
|
13
|
+
* ⚠⚠ MIRROR, not a second design. Every field here exists because it exists
|
|
14
|
+
* in `backend/internal/api/handlers/public_api.go` under the same JSON name
|
|
15
|
+
* (`PublicBranding`, `PublicShare`, `PublicNode`, `PublicApp`,
|
|
16
|
+
* `PublicDrop`). When the Go side grows one, add it here with the same
|
|
17
|
+
* spelling; when the two disagree, the Go file is right.
|
|
18
|
+
*/
|
|
19
|
+
import type { PluginText, PublicPageFile } from './Plugins';
|
|
20
|
+
|
|
21
|
+
/* ── branding ─────────────────────────────────────────────────────────── */
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `GET /api/public/branding` — unauthenticated, cached.
|
|
25
|
+
*
|
|
26
|
+
* ⚠ Deliberately NOT the payload `/api/branding` serves: that one also
|
|
27
|
+
* carries the SSO button label, which a stranger has no business receiving.
|
|
28
|
+
*
|
|
29
|
+
* ⚠⚠ It used to carry the operator's custom stylesheet too, and that is the
|
|
30
|
+
* sentence that was wrong here for a release: the sheet left `/api/branding`
|
|
31
|
+
* for `GET /api/me/custom-css` behind auth, so no anonymous surface — this
|
|
32
|
+
* one included — can be handed a stylesheet that repaints a sign-in form.
|
|
33
|
+
*/
|
|
34
|
+
export interface PublicBranding {
|
|
35
|
+
/** The instance's own name. Empty = the product's. */
|
|
36
|
+
name?: string;
|
|
37
|
+
logo_url?: string;
|
|
38
|
+
/** `#rgb` / `#rrggbb` — becomes `--fe-primary` on the public shell. */
|
|
39
|
+
accent?: string;
|
|
40
|
+
footer_text?: string;
|
|
41
|
+
hide_powered_by?: boolean;
|
|
42
|
+
/** The appearance a visitor gets before they choose: `system` | `light` | `dark`. */
|
|
43
|
+
theme?: 'system' | 'light' | 'dark' | string;
|
|
44
|
+
/** The instance's own default language. */
|
|
45
|
+
locale?: string;
|
|
46
|
+
/** The languages the public pages can be rendered in. */
|
|
47
|
+
locales?: string[];
|
|
48
|
+
/**
|
|
49
|
+
* The languages installed apps add (`manifest.ui_locales`): one row each —
|
|
50
|
+
* `code`, `source: "plugin"`, the `plugin` that brought it, `rtl` — and NO
|
|
51
|
+
* `strings`. One language's strings are `GET /api/public/ui-locales/{code}`
|
|
52
|
+
* (`lib/uiLocales.ensureLocaleStrings`), fetched when it is first used.
|
|
53
|
+
*
|
|
54
|
+
* ⚠⚠ Typed from the SERVER's bytes, not from belief: this field was a map
|
|
55
|
+
* on the wire and a list here for a whole release, and no pack's string
|
|
56
|
+
* ever reached a screen. web/tests/lib/uiLocales.test.ts reads
|
|
57
|
+
* backend/internal/api/handlers/testdata/wire/public-branding.json, which
|
|
58
|
+
* the Go handler's own test writes.
|
|
59
|
+
*/
|
|
60
|
+
ui_locales?: PublicLocaleOption[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* ── the states every public link shares ──────────────────────────────── */
|
|
64
|
+
|
|
65
|
+
/** Why a link draws nothing, for a caller that logs it. */
|
|
66
|
+
export type PublicFailure = '' | 'not_found' | 'gone' | 'error';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What every public link answers, whatever kind it is.
|
|
70
|
+
*
|
|
71
|
+
* ⚠ `expired` and `revoked` are BOTH reported, and they are different facts:
|
|
72
|
+
* the clock ran out, versus the link is dead for another reason (its visit
|
|
73
|
+
* ceiling is spent, the app behind it was stopped, the file is gone). The
|
|
74
|
+
* shell shows one sentence for both — a visitor must not be told which —
|
|
75
|
+
* but the distinction is the server's to make, not the client's to lose.
|
|
76
|
+
*
|
|
77
|
+
* ⚠ `locked` is NOT one of them. It is the PIN gate shut after too many
|
|
78
|
+
* wrong answers, and it lifts by itself.
|
|
79
|
+
*/
|
|
80
|
+
export interface PublicLinkBase {
|
|
81
|
+
needs_pin: boolean;
|
|
82
|
+
unlocked: boolean;
|
|
83
|
+
expired?: boolean;
|
|
84
|
+
revoked?: boolean;
|
|
85
|
+
locked?: boolean;
|
|
86
|
+
/** RFC 3339, or absent for "no expiry". */
|
|
87
|
+
expires_at?: string | null;
|
|
88
|
+
/** Words for the shell's header — a subject line, a file name. */
|
|
89
|
+
subject?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ── a share: `/s/<token>` ────────────────────────────────────────────── */
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `kind` decides the BODY the shell draws and nothing else — the header,
|
|
96
|
+
* the PIN gate, the language picker and the footer are the shell's.
|
|
97
|
+
*
|
|
98
|
+
* file → one document: download it
|
|
99
|
+
* folder → a folder, downloaded whole or walked
|
|
100
|
+
* app → an app plugin's screen, drawn by SurfaceRenderer
|
|
101
|
+
*/
|
|
102
|
+
export type PublicShareKind = 'file' | 'folder' | 'app';
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The little a visitor learns about the thing behind a link: what it is
|
|
106
|
+
* called, how big it is, what kind of thing it is. Never its path, never its
|
|
107
|
+
* storage, never who owns it. Absent until the link is unlocked.
|
|
108
|
+
*/
|
|
109
|
+
export interface PublicNode {
|
|
110
|
+
name: string;
|
|
111
|
+
size?: number;
|
|
112
|
+
mime?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The app surface a link carries, once it is unlocked. */
|
|
116
|
+
export interface PublicApp {
|
|
117
|
+
plugin: string;
|
|
118
|
+
page: string;
|
|
119
|
+
title?: PluginText;
|
|
120
|
+
/** Copies the app exposed to this visitor (`pub:N`). */
|
|
121
|
+
files?: PublicPageFile[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** One row of a folder share's listing, when the server sends one. */
|
|
125
|
+
export interface PublicEntry {
|
|
126
|
+
name: string;
|
|
127
|
+
/** Path relative to the share root, for the browse and download URLs. */
|
|
128
|
+
path: string;
|
|
129
|
+
is_dir: boolean;
|
|
130
|
+
size?: number;
|
|
131
|
+
mime?: string;
|
|
132
|
+
/** Epoch seconds or milliseconds, as the rest of the wire sends them. */
|
|
133
|
+
modified?: number;
|
|
134
|
+
/** Where the browser downloads this entry from; built from the token when absent. */
|
|
135
|
+
url?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `GET /api/public/s/{token}`. */
|
|
139
|
+
export interface PublicShareInfo extends PublicLinkBase {
|
|
140
|
+
kind: PublicShareKind;
|
|
141
|
+
/** Visits left before the link stops answering; null = uncounted. */
|
|
142
|
+
visits_left?: number | null;
|
|
143
|
+
node?: PublicNode | null;
|
|
144
|
+
app?: PublicApp | null;
|
|
145
|
+
/**
|
|
146
|
+
* A folder share's listing.
|
|
147
|
+
*
|
|
148
|
+
* ⚠ Not sent today — the server answers a folder share with its NAME and
|
|
149
|
+
* leaves the walking to the no-JS page. Declared because the shell draws a
|
|
150
|
+
* listing the moment one arrives, and because the alternative (a second
|
|
151
|
+
* browse implementation in the SPA, reading a different endpoint) is the
|
|
152
|
+
* thing this round exists to stop.
|
|
153
|
+
*/
|
|
154
|
+
entries?: PublicEntry[];
|
|
155
|
+
/** The path being listed, relative to the share root. */
|
|
156
|
+
path?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ── a file request: `/d/<token>` ─────────────────────────────────────── */
|
|
160
|
+
|
|
161
|
+
/** What a file request will accept. */
|
|
162
|
+
export interface PublicDropLimits {
|
|
163
|
+
max_files?: number;
|
|
164
|
+
/** Megabytes, as the server states it. */
|
|
165
|
+
max_file_size_mb?: number;
|
|
166
|
+
/** Lower-case extensions, no dot. Empty = anything. */
|
|
167
|
+
allowed_ext?: string[];
|
|
168
|
+
/** The uploader is asked for their name. */
|
|
169
|
+
ask_name?: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** `GET /api/public/d/{token}` — the drop box an outsider uploads into. */
|
|
173
|
+
export interface PublicRequestInfo extends PublicLinkBase {
|
|
174
|
+
kind?: 'drop' | string;
|
|
175
|
+
/** The destination's NAME — never its path, and never a listing. */
|
|
176
|
+
folder?: string;
|
|
177
|
+
/** How many files may still be dropped; null/absent = uncounted. */
|
|
178
|
+
uploads_left?: number | null;
|
|
179
|
+
limits?: PublicDropLimits;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* ── languages ────────────────────────────────────────────────────────── */
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* One entry of the language list the interface offers.
|
|
186
|
+
*
|
|
187
|
+
* ⚠ The list is NOT a constant any more (v3 §5). It is the product's own
|
|
188
|
+
* languages plus whatever the server adds — including a language an app
|
|
189
|
+
* plugin ships (`manifest.ui_locales`), which is marked with the app it came
|
|
190
|
+
* from and leaves with it. A key the extra language does not carry falls
|
|
191
|
+
* back to English, exactly as a missing key always has.
|
|
192
|
+
*/
|
|
193
|
+
export interface PublicLocaleOption {
|
|
194
|
+
/** BCP-47 primary subtag, e.g. `en`, `tr`, `ar`. */
|
|
195
|
+
code: string;
|
|
196
|
+
/** The language's name IN that language ("Türkçe"), not in the viewer's. */
|
|
197
|
+
label?: string;
|
|
198
|
+
/** `builtin` = shipped with filex; `plugin` = added by an app. */
|
|
199
|
+
source?: 'builtin' | 'plugin' | string;
|
|
200
|
+
/** The app that ships it, when `source` is `plugin` — shown beside the name. */
|
|
201
|
+
plugin?: string;
|
|
202
|
+
/**
|
|
203
|
+
* That language's strings by filex's own keys — absent on the branding
|
|
204
|
+
* list, present once `ensureLocaleStrings` has fetched them.
|
|
205
|
+
*/
|
|
206
|
+
strings?: Record<string, string>;
|
|
207
|
+
/**
|
|
208
|
+
* Written right to left (Arabic, Hebrew, Persian, Urdu, …): the page's
|
|
209
|
+
* `dir` follows (lib/direction, docs/RTL.md).
|
|
210
|
+
*/
|
|
211
|
+
rtl?: boolean;
|
|
212
|
+
}
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { computed, onMounted, ref, watch } from 'vue';
|
|
13
13
|
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
14
14
|
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
15
|
+
import DataTable, { type DataColumn } from '../components/DataTable.vue';
|
|
16
|
+
import { useLocale } from '../composables/useLocale';
|
|
15
17
|
|
|
16
18
|
interface ArchiveEntry {
|
|
17
19
|
name: string;
|
|
@@ -25,6 +27,9 @@ const props = defineProps<{
|
|
|
25
27
|
filePath?: string;
|
|
26
28
|
ext: string;
|
|
27
29
|
t?: (key: string, vars?: Record<string, string | number>) => string;
|
|
30
|
+
/** The interface language — the size column is written the way it writes
|
|
31
|
+
* numbers ("1,96 KB"), like every other size in the product. */
|
|
32
|
+
locale?: string;
|
|
28
33
|
authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
29
34
|
authCredentials?: RequestCredentials;
|
|
30
35
|
/** ⚠ Where to ask. This used to be the literal '/api/files/archive/list',
|
|
@@ -46,11 +51,13 @@ function tt(key: string, fallback: string, vars?: Record<string, string | number
|
|
|
46
51
|
return props.t ? props.t(key, vars) : fallback;
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
/* ⚠ The product's one byte formatter (useLocale.formatSize). This file had
|
|
55
|
+
its own — base 1024, a dot, English units — so a zip's member read
|
|
56
|
+
"1.9 KB" beside the explorer's "1,96 KB" for the same bytes (QA,
|
|
57
|
+
2026-09-21). */
|
|
58
|
+
const { formatSize } = useLocale(() => props.locale ?? 'en');
|
|
49
59
|
function fmtSize(n: number): string {
|
|
50
|
-
|
|
51
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
52
|
-
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
53
|
-
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
60
|
+
return formatSize(n);
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
async function load(): Promise<void> {
|
|
@@ -76,20 +83,78 @@ async function load(): Promise<void> {
|
|
|
76
83
|
body: JSON.stringify({ path: props.filePath }),
|
|
77
84
|
});
|
|
78
85
|
if (!res.ok) {
|
|
79
|
-
|
|
86
|
+
/* ⚠ Never the raw status. This printed "503 Service Unavailable" (or a
|
|
87
|
+
* JSON body) straight into the preview — the same failure the owner
|
|
88
|
+
* reported for ONLYOFFICE ("Config fetch 503: {…}"): a person is shown
|
|
89
|
+
* the HTTP layer instead of what happened. The server's reason, when it
|
|
90
|
+
* sent one we know, becomes a sentence; anything else is the generic
|
|
91
|
+
* "could not read the archive". */
|
|
92
|
+
throw new ArchiveError(await archiveErrorText(res));
|
|
80
93
|
}
|
|
81
94
|
const body = (await res.json()) as { entries?: ArchiveEntry[] };
|
|
82
95
|
entries.value = body.entries ?? [];
|
|
83
96
|
} catch (err) {
|
|
84
|
-
error.value =
|
|
97
|
+
error.value =
|
|
98
|
+
err instanceof ArchiveError ? err.message : tt('viewer.archive.error', 'Could not read archive contents.');
|
|
85
99
|
} finally {
|
|
86
100
|
loading.value = false;
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
|
|
104
|
+
/** A failure already worded for a person. Anything else that throws (the
|
|
105
|
+
* network, a JSON parse) reads as the generic sentence. */
|
|
106
|
+
class ArchiveError extends Error {}
|
|
107
|
+
|
|
108
|
+
async function archiveErrorText(res: Response): Promise<string> {
|
|
109
|
+
let code = '';
|
|
110
|
+
try {
|
|
111
|
+
const body = (await res.json()) as { error?: unknown; code?: unknown };
|
|
112
|
+
code = String(body?.code ?? body?.error ?? '');
|
|
113
|
+
} catch {
|
|
114
|
+
/* not JSON — the generic sentence below */
|
|
115
|
+
}
|
|
116
|
+
if (/not a zip|unsupported|not supported|format/i.test(code)) {
|
|
117
|
+
return tt('viewer.archive.unsupported', 'This kind of archive cannot be listed here. Download it to see what is inside.');
|
|
118
|
+
}
|
|
119
|
+
if (res.status === 413 || /too large|too big/i.test(code)) {
|
|
120
|
+
return tt('viewer.archive.too_large', 'This archive is too large to list here. Download it to see what is inside.');
|
|
121
|
+
}
|
|
122
|
+
return tt('viewer.archive.error', 'Could not read archive contents.');
|
|
123
|
+
}
|
|
124
|
+
|
|
90
125
|
onMounted(load);
|
|
91
126
|
watch(() => props.filePath, load);
|
|
92
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The entries, in THE table (DataTable — the explorer's own). ⚠ It used to be
|
|
130
|
+
* the one table in the package allowed to be its own, with the reasoning that
|
|
131
|
+
* a file's contents answer to the viewer's layout. The owner's rule since
|
|
132
|
+
* 2026-09-21 has no such exception — "bir yere tablo gerekiyorsa bu tabloyu
|
|
133
|
+
* koymak zorundayız" — and an archive's listing is a file listing: it wants
|
|
134
|
+
* to be sorted by size and have its Name column widened exactly the way the
|
|
135
|
+
* folder it came from does.
|
|
136
|
+
*/
|
|
137
|
+
const columns = computed<DataColumn<ArchiveEntry>[]>(() => [
|
|
138
|
+
{
|
|
139
|
+
id: 'name',
|
|
140
|
+
label: tt('viewer.name', 'Name'),
|
|
141
|
+
sortable: true,
|
|
142
|
+
width: 360,
|
|
143
|
+
class: 'filex-viewer-archive__name',
|
|
144
|
+
title: (e) => e.name,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'size',
|
|
148
|
+
label: tt('viewer.size', 'Size'),
|
|
149
|
+
sortable: true,
|
|
150
|
+
width: 110,
|
|
151
|
+
align: 'right',
|
|
152
|
+
class: 'filex-viewer-archive__size',
|
|
153
|
+
format: (e) => (e.is_dir ? '' : fmtSize(e.size)),
|
|
154
|
+
sortValue: (e) => (e.is_dir ? -1 : e.size),
|
|
155
|
+
},
|
|
156
|
+
]);
|
|
157
|
+
|
|
93
158
|
const totalSize = computed(() => entries.value.reduce((sum, e) => sum + (e.size || 0), 0));
|
|
94
159
|
const fileCount = computed(() => entries.value.filter((e) => !e.is_dir).length);
|
|
95
160
|
|
|
@@ -142,27 +207,22 @@ function entryTile(e: ArchiveEntry): string {
|
|
|
142
207
|
{{ tt('viewer.archive.entries', `${fileCount} files`, { n: fileCount }) }}
|
|
143
208
|
· {{ fmtSize(totalSize) }}
|
|
144
209
|
</div>
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
</td>
|
|
162
|
-
<td class="filex-viewer-archive__size">{{ e.is_dir ? '' : fmtSize(e.size) }}</td>
|
|
163
|
-
</tr>
|
|
164
|
-
</tbody>
|
|
165
|
-
</table>
|
|
210
|
+
<DataTable
|
|
211
|
+
table-id="viewer.archive"
|
|
212
|
+
:columns="columns"
|
|
213
|
+
:rows="entries"
|
|
214
|
+
:row-key="(e: ArchiveEntry) => e.name"
|
|
215
|
+
:row-attrs="(e: ArchiveEntry) => ({ 'data-dir': e.is_dir ? '1' : '0' })"
|
|
216
|
+
>
|
|
217
|
+
<template #cell-name="{ row }">
|
|
218
|
+
<!-- ikon:emoji — the same tile the listing draws for the same kind
|
|
219
|
+
of file, so an archive's contents and the folder it came from
|
|
220
|
+
read as one product. -->
|
|
221
|
+
<!-- eslint-disable-next-line vue/no-v-html -- static markup from lib/fileIcons -->
|
|
222
|
+
<span class="filex-viewer-archive__icon" aria-hidden="true" v-html="entryTile(row)"></span>
|
|
223
|
+
<span class="fe-list__cell-text">{{ row.name }}</span>
|
|
224
|
+
</template>
|
|
225
|
+
</DataTable>
|
|
166
226
|
</div>
|
|
167
227
|
</div>
|
|
168
228
|
</template>
|
|
@@ -196,34 +256,19 @@ function entryTile(e: ArchiveEntry): string {
|
|
|
196
256
|
margin-bottom: 12px;
|
|
197
257
|
font-variant-numeric: tabular-nums;
|
|
198
258
|
}
|
|
199
|
-
.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
font-size: 13px;
|
|
203
|
-
font-family: var(--fe-font-mono, monospace);
|
|
204
|
-
}
|
|
205
|
-
.filex-viewer-archive__table th,
|
|
206
|
-
.filex-viewer-archive__table td {
|
|
207
|
-
padding: 6px 12px;
|
|
208
|
-
border-bottom: 1px solid var(--fe-border, #e2e6ed);
|
|
209
|
-
text-align: left;
|
|
210
|
-
}
|
|
211
|
-
.filex-viewer-archive__table th {
|
|
212
|
-
background: var(--fe-bg-elev, #f7f8fa);
|
|
213
|
-
font-weight: 600;
|
|
214
|
-
position: sticky;
|
|
215
|
-
top: 0;
|
|
216
|
-
}
|
|
259
|
+
/* The table itself is DataTable's (styles/base.css); only the viewer's own
|
|
260
|
+
touches stay here — tabular figures and muted sizes, a folder row read as
|
|
261
|
+
quieter than a file. */
|
|
217
262
|
.filex-viewer-archive__size {
|
|
218
|
-
text-align:
|
|
263
|
+
text-align: end;
|
|
219
264
|
font-variant-numeric: tabular-nums;
|
|
220
265
|
white-space: nowrap;
|
|
221
266
|
color: var(--fe-text-muted, #5a6475);
|
|
222
267
|
}
|
|
223
268
|
.filex-viewer-archive__icon {
|
|
224
|
-
margin-
|
|
269
|
+
margin-inline-end: 6px;
|
|
225
270
|
}
|
|
226
|
-
.filex-viewer-
|
|
271
|
+
.filex-viewer-archive .fe-list__row[data-dir="1"] {
|
|
227
272
|
color: var(--fe-text-muted, #5a6475);
|
|
228
273
|
}
|
|
229
274
|
.filex-viewer-fallback {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { sayFailure } from '../lib/errorWords';
|
|
2
3
|
/**
|
|
3
4
|
* CsvViewer — lightweight CSV / TSV table preview.
|
|
4
5
|
*
|
|
@@ -17,6 +18,7 @@ import { computed, onMounted, ref, watch } from 'vue';
|
|
|
17
18
|
import { actionIconSvg } from '../lib/actionIcons'; /* ikon:emoji */
|
|
18
19
|
import { fileIconTile } from '../lib/fileIcons'; /* ikon:emoji */
|
|
19
20
|
import { fetchViewerText } from '../composables/useViewerFetch';
|
|
21
|
+
import DataTable, { type DataColumn } from '../components/DataTable.vue';
|
|
20
22
|
|
|
21
23
|
const props = defineProps<{
|
|
22
24
|
url: string;
|
|
@@ -86,7 +88,7 @@ async function load(): Promise<void> {
|
|
|
86
88
|
credentials: props.authCredentials,
|
|
87
89
|
});
|
|
88
90
|
} catch (err) {
|
|
89
|
-
error.value = err
|
|
91
|
+
error.value = sayFailure(err, tt('viewer.failed_to_load', 'Failed to load file'), { t: props.t }).text;
|
|
90
92
|
loading.value = false;
|
|
91
93
|
return;
|
|
92
94
|
}
|
|
@@ -125,27 +127,93 @@ const dataRows = computed<string[][]>(() => {
|
|
|
125
127
|
return firstRowHeader.value ? rows.value.slice(1) : rows.value;
|
|
126
128
|
});
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
/** A parsed row with the line it came from, so `#` keeps naming the row in
|
|
131
|
+
* the FILE after a sort re-orders the screen. */
|
|
132
|
+
interface CsvRow {
|
|
133
|
+
n: number;
|
|
134
|
+
cells: string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const filtered = computed<CsvRow[]>(() => {
|
|
129
138
|
const q = filter.value.trim().toLowerCase();
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
139
|
+
const all = dataRows.value.map((cells, i) => ({ n: i + 1, cells }));
|
|
140
|
+
if (!q) return all;
|
|
141
|
+
return all.filter((r) => r.cells.some((c) => (c || '').toLowerCase().includes(q)));
|
|
134
142
|
});
|
|
135
143
|
|
|
144
|
+
/**
|
|
145
|
+
* THE SORT, done here and handed to the table as CONTROLLED.
|
|
146
|
+
*
|
|
147
|
+
* ⚠⚠ Here and not in the table, because the table only ever holds one page
|
|
148
|
+
* (100 rows) of the parsed file, and a table that sorts the page it holds
|
|
149
|
+
* would call a re-ordered 100 of 1000 "sorted" — so it rightly closes its
|
|
150
|
+
* headers over a paged list. This component holds EVERY parsed row, so it can
|
|
151
|
+
* sort all of them and then page, which is the honest order.
|
|
152
|
+
*/
|
|
153
|
+
const sort = ref<{ key: string; dir: 'asc' | 'desc' } | null>(null);
|
|
154
|
+
|
|
155
|
+
function cellOf(r: CsvRow, key: string): string {
|
|
156
|
+
if (key === 'n') return String(r.n);
|
|
157
|
+
return r.cells[Number(key.slice(1))] ?? '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const sorted = computed<CsvRow[]>(() => {
|
|
161
|
+
const s = sort.value;
|
|
162
|
+
if (!s) return filtered.value;
|
|
163
|
+
const dir = s.dir === 'asc' ? 1 : -1;
|
|
164
|
+
return [...filtered.value].sort((a, b) => {
|
|
165
|
+
if (s.key === 'n') return dir * (a.n - b.n);
|
|
166
|
+
const x = cellOf(a, s.key);
|
|
167
|
+
const y = cellOf(b, s.key);
|
|
168
|
+
// Empty cells last in both directions; numbers as numbers.
|
|
169
|
+
if (!x !== !y) return x ? -1 : 1;
|
|
170
|
+
const nx = Number(x);
|
|
171
|
+
const ny = Number(y);
|
|
172
|
+
if (x && y && Number.isFinite(nx) && Number.isFinite(ny)) return dir * (nx - ny) || a.n - b.n;
|
|
173
|
+
return dir * x.localeCompare(y, undefined, { numeric: true, sensitivity: 'base' }) || a.n - b.n;
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
function onSort(next: { key: string; dir: 'asc' | 'desc' }) {
|
|
178
|
+
sort.value = next;
|
|
179
|
+
}
|
|
180
|
+
|
|
136
181
|
const totalPages = computed(() =>
|
|
137
|
-
Math.max(1, Math.ceil(
|
|
182
|
+
Math.max(1, Math.ceil(sorted.value.length / PAGE_SIZE)),
|
|
138
183
|
);
|
|
139
184
|
|
|
140
|
-
const visibleRows = computed<
|
|
185
|
+
const visibleRows = computed<CsvRow[]>(() => {
|
|
141
186
|
const start = (page.value - 1) * PAGE_SIZE;
|
|
142
|
-
return
|
|
187
|
+
return sorted.value.slice(start, start + PAGE_SIZE);
|
|
143
188
|
});
|
|
144
189
|
|
|
145
|
-
watch(filter, () => {
|
|
190
|
+
watch([filter, sort], () => {
|
|
146
191
|
page.value = 1;
|
|
147
192
|
});
|
|
148
193
|
|
|
194
|
+
/** The file's columns, as THE table (DataTable — the explorer's own) draws
|
|
195
|
+
* them. `#` is the lead: it is what names a row of a spreadsheet. */
|
|
196
|
+
const columns = computed<DataColumn<CsvRow>[]>(() => [
|
|
197
|
+
{
|
|
198
|
+
id: 'n',
|
|
199
|
+
label: '#',
|
|
200
|
+
lead: true,
|
|
201
|
+
width: 64,
|
|
202
|
+
min: 48,
|
|
203
|
+
align: 'right',
|
|
204
|
+
sortable: true,
|
|
205
|
+
class: 'filex-viewer-csv__rownum',
|
|
206
|
+
format: (r) => r.n,
|
|
207
|
+
},
|
|
208
|
+
...headers.value.map((h, i) => ({
|
|
209
|
+
id: `c${i}`,
|
|
210
|
+
label: h,
|
|
211
|
+
width: 140,
|
|
212
|
+
sortable: true,
|
|
213
|
+
format: (r: CsvRow) => r.cells[i] ?? '',
|
|
214
|
+
})),
|
|
215
|
+
]);
|
|
216
|
+
|
|
149
217
|
function tt(key: string, fallback: string, vars?: Record<string, string | number>): string {
|
|
150
218
|
return props.t ? props.t(key, vars) : fallback;
|
|
151
219
|
}
|
|
@@ -177,20 +245,19 @@ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ex
|
|
|
177
245
|
></span>
|
|
178
246
|
<p>{{ tt('viewer.loading', 'Loading…') }}</p>
|
|
179
247
|
</div>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
</table>
|
|
248
|
+
<!-- ⚠ No table id: a spreadsheet's columns are whatever THIS file has
|
|
249
|
+
(`c0`, `c1`, …), so widths remembered against them would land on a
|
|
250
|
+
different file's different columns next time. The arrangement
|
|
251
|
+
lives for this preview. -->
|
|
252
|
+
<DataTable
|
|
253
|
+
v-else
|
|
254
|
+
:table-id="undefined"
|
|
255
|
+
:columns="columns"
|
|
256
|
+
:rows="visibleRows"
|
|
257
|
+
:row-key="(r: CsvRow) => r.n"
|
|
258
|
+
:sort="sort"
|
|
259
|
+
@sort="onSort"
|
|
260
|
+
/>
|
|
194
261
|
</div>
|
|
195
262
|
<div v-if="!loading && !error && totalPages > 1" class="filex-viewer-csv__pager">
|
|
196
263
|
<button
|
|
@@ -199,7 +266,7 @@ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ex
|
|
|
199
266
|
:disabled="page <= 1"
|
|
200
267
|
@click="page--"
|
|
201
268
|
>‹</button>
|
|
202
|
-
<span class="filex-viewer-csv__pageno">{{ page }} / {{ totalPages }} ({{ tt('viewer.csv_rows', `${filtered.length} rows`, { n: filtered.length }) }})</span>
|
|
269
|
+
<span class="filex-viewer-csv__pageno"><bdi dir="ltr">{{ page }} / {{ totalPages }}</bdi> ({{ tt('viewer.csv_rows', `${filtered.length} rows`, { n: filtered.length }) }})</span>
|
|
203
270
|
<button
|
|
204
271
|
type="button"
|
|
205
272
|
class="filex-viewer-btn"
|
|
@@ -266,39 +333,18 @@ const typeTile = computed(() => fileIconTile({ type: 'file', extension: props.ex
|
|
|
266
333
|
flex: 1;
|
|
267
334
|
overflow: auto;
|
|
268
335
|
}
|
|
269
|
-
.filex-viewer-
|
|
270
|
-
border-collapse: collapse;
|
|
271
|
-
width: max-content;
|
|
272
|
-
min-width: 100%;
|
|
273
|
-
font-size: 12px;
|
|
336
|
+
.filex-viewer-csv__pane .fe-list__cell {
|
|
274
337
|
font-family: var(--fe-font-mono, monospace);
|
|
338
|
+
font-size: 12px;
|
|
275
339
|
}
|
|
276
|
-
.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
max-width: 320px;
|
|
282
|
-
overflow: hidden;
|
|
283
|
-
text-overflow: ellipsis;
|
|
284
|
-
}
|
|
285
|
-
.filex-viewer-csv__table th {
|
|
286
|
-
background: var(--fe-bg-elev, #f7f8fa);
|
|
287
|
-
position: sticky;
|
|
288
|
-
top: 0;
|
|
289
|
-
font-weight: 600;
|
|
290
|
-
text-align: left;
|
|
291
|
-
}
|
|
292
|
-
.filex-viewer-csv__table tbody tr:nth-child(even) {
|
|
293
|
-
background: var(--fe-bg-hover, rgba(0, 0, 0, 0.02));
|
|
294
|
-
}
|
|
340
|
+
/* The table itself is DataTable's (styles/base.css) — the product's one table.
|
|
341
|
+
⚠ The row number used to pin itself (`position: sticky; left: 0`) because
|
|
342
|
+
this was a table of its own; it is the table's LEAD now, and the table
|
|
343
|
+
freezes its lead itself, only while there is room to (a pin written here
|
|
344
|
+
would fight that gate). What stays is the look of a row number. */
|
|
295
345
|
.filex-viewer-csv__rownum {
|
|
296
346
|
color: var(--fe-text-muted, #5a6475);
|
|
297
|
-
text-align: right;
|
|
298
|
-
background: var(--fe-bg-elev, #f7f8fa);
|
|
299
347
|
font-variant-numeric: tabular-nums;
|
|
300
|
-
position: sticky;
|
|
301
|
-
left: 0;
|
|
302
348
|
}
|
|
303
349
|
.filex-viewer-csv__pager {
|
|
304
350
|
display: flex;
|