@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,57 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The filex mark — the ONE copy of it in the product's own code.
|
|
3
|
+
//
|
|
4
|
+
// A tiny folder + checkmark: recognisable as a file manager, inline SVG so it
|
|
5
|
+
// needs no asset pipeline. `class` falls through to the root element, so a
|
|
6
|
+
// caller sizes it.
|
|
7
|
+
//
|
|
8
|
+
// ⚠⚠ It lives in `packages/core` and not in `web/` because the public shell
|
|
9
|
+
// (`components/public/PublicShell.vue`) draws it too, and `packages/core`
|
|
10
|
+
// cannot import out of the web app. `web/src/components/LogoMark.vue` is a
|
|
11
|
+
// re-export of this file; every existing call site keeps working.
|
|
12
|
+
// `scripts/dup-scan.mjs` names THIS path as the concept's home
|
|
13
|
+
// (`brand-mark`), and `web/tests/quality/duplication.test.ts` fails on a
|
|
14
|
+
// second hand-typed copy — for a reason with a date on it: the public share
|
|
15
|
+
// page carried the pre-rebrand indigo for a whole release because its mark
|
|
16
|
+
// was re-typed in Go rather than generated from here.
|
|
17
|
+
//
|
|
18
|
+
// gorunum:v1 — flat product blue, not the old indigo→violet gradient ("maviye
|
|
19
|
+
// çekelim"). The fill is the palette's own `--fe-primary`, so the mark is
|
|
20
|
+
// #2f6ceb on a light page and #5b8cff on a dark one, and it follows any theme
|
|
21
|
+
// the product ever ships rather than being a colour no theme can reach. On a
|
|
22
|
+
// public link with an instance accent set the same token carries the
|
|
23
|
+
// operator's colour (`accentStyleOf`), which is the behaviour a brandable
|
|
24
|
+
// mark should have.
|
|
25
|
+
//
|
|
26
|
+
// ⚠ The literal is a floor, not the value in use: `fill` has no inherited
|
|
27
|
+
// value to fall back to, so an unstyled context (core's stylesheet missing)
|
|
28
|
+
// would paint the mark BLACK rather than blue. Measured in the browser on
|
|
29
|
+
// /admin/login, /admin/dashboard and /admin/about: the computed fill is the
|
|
30
|
+
// token in all three, in both variants — the literal never renders today.
|
|
31
|
+
//
|
|
32
|
+
// The same blue is written out by hand in three static files that have no
|
|
33
|
+
// token scope at all: web/public/favicon.svg, web/public/icons/icon.svg and
|
|
34
|
+
// the `theme-color` meta in web/index.html (plus the PWA manifest's
|
|
35
|
+
// theme_color in web/vite.config.ts). Change one, change all four.
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<svg viewBox="0 0 32 32" aria-hidden="true">
|
|
40
|
+
<rect width="32" height="32" rx="7" fill="var(--fe-primary, #2f6ceb)" />
|
|
41
|
+
<path
|
|
42
|
+
d="M7 11a2 2 0 0 1 2-2h5l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2V11z"
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="#fff"
|
|
45
|
+
stroke-width="1.8"
|
|
46
|
+
stroke-linejoin="round"
|
|
47
|
+
/>
|
|
48
|
+
<path
|
|
49
|
+
d="M11.5 17.5l3 2.5 5.5-6"
|
|
50
|
+
fill="none"
|
|
51
|
+
stroke="#fff"
|
|
52
|
+
stroke-width="1.8"
|
|
53
|
+
stroke-linecap="round"
|
|
54
|
+
stroke-linejoin="round"
|
|
55
|
+
/>
|
|
56
|
+
</svg>
|
|
57
|
+
</template>
|
|
@@ -16,6 +16,8 @@ import { computed, onMounted, ref } from 'vue';
|
|
|
16
16
|
import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
|
|
17
17
|
import type { NFSExport } from '../types/NFSExports';
|
|
18
18
|
import { useLocale } from '../composables/useLocale';
|
|
19
|
+
import DataTable, { type DataColumn } from './DataTable.vue';
|
|
20
|
+
import type { ContextAction } from './ContextMenu.vue';
|
|
19
21
|
import { useNFSExports } from '../composables/useNFSExports';
|
|
20
22
|
import { resolveLocale } from '../locales/resolve';
|
|
21
23
|
|
|
@@ -94,6 +96,61 @@ async function mint() {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/** The row's verbs, behind its one `Actions` control. ⚠ `revoke` keeps the
|
|
100
|
+
* two-step confirmation it had as a loose button. */
|
|
101
|
+
function rowActions(e: NFSExport): ContextAction[] {
|
|
102
|
+
/* The whole control used to be `:disabled="busy"`; the table draws the
|
|
103
|
+
control now, so each verb carries it. */
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
key: 'toggle',
|
|
107
|
+
label: e.disabled_at ? t('conn.nfs.enable') : t('conn.nfs.disable'),
|
|
108
|
+
icon: e.disabled_at ? 'check' : 'lock',
|
|
109
|
+
disabled: busy.value,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: 'revoke',
|
|
113
|
+
label: confirmRemove.value === e.id ? t('conn.nfs.confirm') : t('conn.nfs.revoke'),
|
|
114
|
+
icon: 'delete',
|
|
115
|
+
danger: true,
|
|
116
|
+
disabled: busy.value,
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The list's columns — the product's one table (DataTable). */
|
|
122
|
+
const columns = computed<DataColumn<NFSExport>[]>(() => [
|
|
123
|
+
{
|
|
124
|
+
id: 'label',
|
|
125
|
+
label: t('conn.nfs.col.label'),
|
|
126
|
+
sortable: true,
|
|
127
|
+
width: 180,
|
|
128
|
+
format: (e) => e.label || t('conn.nfs.noLabel'),
|
|
129
|
+
},
|
|
130
|
+
{ id: 'scope', label: t('conn.nfs.col.scope'), sortable: true, width: 180, format: scopeOf },
|
|
131
|
+
{
|
|
132
|
+
id: 'mode',
|
|
133
|
+
label: t('conn.nfs.col.mode'),
|
|
134
|
+
sortable: true,
|
|
135
|
+
width: 120,
|
|
136
|
+
format: (e) => (e.read_only ? t('conn.nfs.modeRead') : t('conn.nfs.modeWrite')),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'lastUsed',
|
|
140
|
+
label: t('conn.nfs.col.lastUsed'),
|
|
141
|
+
sortable: true,
|
|
142
|
+
sortDir: 'desc',
|
|
143
|
+
width: 140,
|
|
144
|
+
format: (e) => (e.last_used_at ? shortDate(e.last_used_at) : t('conn.nfs.neverUsed')),
|
|
145
|
+
sortValue: (e) => (e.last_used_at ? new Date(e.last_used_at).getTime() : null),
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
function onRowAction(key: string, e: NFSExport) {
|
|
150
|
+
if (key === 'toggle') void toggle(e);
|
|
151
|
+
else if (key === 'revoke') void drop(e);
|
|
152
|
+
}
|
|
153
|
+
|
|
97
154
|
async function toggle(e: NFSExport) {
|
|
98
155
|
busy.value = true;
|
|
99
156
|
try {
|
|
@@ -234,35 +291,26 @@ function scopeOf(e: NFSExport): string {
|
|
|
234
291
|
</button>
|
|
235
292
|
</div>
|
|
236
293
|
|
|
294
|
+
<!-- ⚠ THE table (DataTable — the explorer's own), not a table of its own:
|
|
295
|
+
reachable from the admin panel's Connections menu and from the
|
|
296
|
+
explorer, so it resizes, sorts, hides and moves columns and remembers
|
|
297
|
+
that on the account (`conn.nfs`), like every other table. (It once had
|
|
298
|
+
no scroll container at all, so its actions cell could not pin; the
|
|
299
|
+
one table owns that now.) -->
|
|
237
300
|
<p v-if="loading" class="fe-s3keys__muted">…</p>
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<td>{{ scopeOf(e) }}</td>
|
|
252
|
-
<td>{{ e.read_only ? t('conn.nfs.modeRead') : t('conn.nfs.modeWrite') }}</td>
|
|
253
|
-
<td>{{ e.last_used_at ? shortDate(e.last_used_at) : t('conn.nfs.neverUsed') }}</td>
|
|
254
|
-
<td class="fe-s3keys__actions">
|
|
255
|
-
<button class="fe-s3keys__link" :disabled="busy" @click="toggle(e)">
|
|
256
|
-
{{ e.disabled_at ? t('conn.nfs.enable') : t('conn.nfs.disable') }}
|
|
257
|
-
</button>
|
|
258
|
-
<button class="fe-s3keys__link is-danger" :disabled="busy" @click="drop(e)">
|
|
259
|
-
{{ confirmRemove === e.id ? t('conn.nfs.confirm') : t('conn.nfs.revoke') }}
|
|
260
|
-
</button>
|
|
261
|
-
</td>
|
|
262
|
-
</tr>
|
|
263
|
-
</tbody>
|
|
264
|
-
</table>
|
|
265
|
-
<p v-else-if="canMint" class="fe-s3keys__muted">{{ t('conn.nfs.empty') }}</p>
|
|
301
|
+
<DataTable
|
|
302
|
+
v-else
|
|
303
|
+
table-id="conn.nfs"
|
|
304
|
+
:columns="columns"
|
|
305
|
+
:rows="exports"
|
|
306
|
+
row-key="id"
|
|
307
|
+
:locale="locale"
|
|
308
|
+
:empty="t('conn.nfs.empty')"
|
|
309
|
+
:row-class="(e: NFSExport) => (e.disabled_at ? 'is-muted' : undefined)"
|
|
310
|
+
:row-actions="rowActions"
|
|
311
|
+
:row-actions-test-id="(e: NFSExport) => `nfs-export-actions-${e.id}`"
|
|
312
|
+
@row-action="(key: string, e: NFSExport) => onRowAction(key, e)"
|
|
313
|
+
/>
|
|
266
314
|
</section>
|
|
267
315
|
</template>
|
|
268
316
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
20
20
|
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
21
21
|
import { useLocale } from '../composables/useLocale';
|
|
22
|
+
import { inlineKeyStep } from '../lib/direction';
|
|
22
23
|
import { shortcutHint } from '../composables/useKeyboardShortcuts';
|
|
23
24
|
|
|
24
25
|
const props = defineProps<{
|
|
@@ -33,7 +34,8 @@ const emit = defineEmits<{
|
|
|
33
34
|
(e: 'close'): void;
|
|
34
35
|
}>();
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
// ⚠ RTL: `dir` goes on the teleported tour and orders its choices below.
|
|
38
|
+
const { t, dir } = useLocale(() => props.locale);
|
|
37
39
|
|
|
38
40
|
/* The tour teaches two keys by name, and both are remappable. Read them
|
|
39
41
|
* from the registry so the sentence stays true for whoever is taking the
|
|
@@ -260,6 +262,14 @@ async function place() {
|
|
|
260
262
|
const midX = clampX(rect.left + rect.width / 2 - cw / 2);
|
|
261
263
|
const midY = clampY(rect.top + rect.height / 2 - ch / 2);
|
|
262
264
|
|
|
265
|
+
/* Beside it: toward the inline END first — right in LTR, LEFT in RTL, where
|
|
266
|
+
the reading eye goes next. ⚠ The spotlight and these numbers stay
|
|
267
|
+
PHYSICAL (they are the target's own viewport box); only which side is
|
|
268
|
+
tried first follows the direction. */
|
|
269
|
+
const toRight = { fits: sRight + GAP + cw <= vw - 12, left: sRight + GAP };
|
|
270
|
+
const toLeft = { fits: sLeft - GAP - cw >= 12, left: sLeft - GAP - cw };
|
|
271
|
+
const beside = dir.value === 'rtl' ? [toLeft, toRight] : [toRight, toLeft];
|
|
272
|
+
|
|
263
273
|
let top: number;
|
|
264
274
|
let left: number;
|
|
265
275
|
if (sBottom + GAP + ch <= vh - 12) {
|
|
@@ -268,11 +278,11 @@ async function place() {
|
|
|
268
278
|
} else if (sTop - GAP - ch >= 12) {
|
|
269
279
|
top = sTop - GAP - ch;
|
|
270
280
|
left = midX;
|
|
271
|
-
} else if (
|
|
272
|
-
left =
|
|
281
|
+
} else if (beside[0].fits) {
|
|
282
|
+
left = beside[0].left;
|
|
273
283
|
top = midY;
|
|
274
|
-
} else if (
|
|
275
|
-
left =
|
|
284
|
+
} else if (beside[1].fits) {
|
|
285
|
+
left = beside[1].left;
|
|
276
286
|
top = midY;
|
|
277
287
|
} else {
|
|
278
288
|
// Nothing fits — a target that fills the viewport. Clamp, and accept the
|
|
@@ -338,10 +348,11 @@ function onKey(e: KeyboardEvent) {
|
|
|
338
348
|
if (e.key === 'Escape') {
|
|
339
349
|
e.stopPropagation();
|
|
340
350
|
skip();
|
|
341
|
-
} else if (e.key === '
|
|
351
|
+
} else if (e.key === 'Enter' || inlineKeyStep(e.key, dir.value) === 1) {
|
|
352
|
+
// ⚠ RTL: "forward" is the arrow that points the way the line reads — ←.
|
|
342
353
|
e.preventDefault();
|
|
343
354
|
next();
|
|
344
|
-
} else if (e.key ===
|
|
355
|
+
} else if (inlineKeyStep(e.key, dir.value) === -1) {
|
|
345
356
|
e.preventDefault();
|
|
346
357
|
back();
|
|
347
358
|
}
|
|
@@ -399,6 +410,7 @@ const themeClass = computed(() => `fe-ctx-backdrop--theme-${props.theme || 'auto
|
|
|
399
410
|
<div
|
|
400
411
|
v-if="open && step"
|
|
401
412
|
class="fe-tour"
|
|
413
|
+
:dir="dir"
|
|
402
414
|
:class="themeClass"
|
|
403
415
|
:data-prefers-dark="prefersDark ? '1' : '0'"
|
|
404
416
|
role="dialog"
|
|
@@ -65,6 +65,11 @@ const KIND_ICONS: Record<Operation['kind'], string> = {
|
|
|
65
65
|
delete: 'M5 7h14M9 7V5h6v2M8 7l1 13h6l1-13',
|
|
66
66
|
convert: 'M20 8A8 8 0 0 0 6 6L4 8M4 16a8 8 0 0 0 14 2l2-2M20 3v5h-5M4 21v-5h5',
|
|
67
67
|
archive: 'M4 8V5h16v3zM5 8h14v12H5zM10 12h4',
|
|
68
|
+
/* "Empty the trash" — the bin, with the lines of what is going. */
|
|
69
|
+
trash: 'M5 7h14M9 7V5h6v2M8 7l1 13h6l1-13M10.5 11v5M13.5 11v5',
|
|
70
|
+
/* An app-plugin job — the puzzle piece its menu row wears (lib/actionIcons). */
|
|
71
|
+
plugin:
|
|
72
|
+
'M9.5 4.5a2 2 0 1 1 4 0h3a1.5 1.5 0 0 1 1.5 1.5v3a2 2 0 1 1 0 4v3a1.5 1.5 0 0 1-1.5 1.5h-3a2 2 0 1 1-4 0h-3A1.5 1.5 0 0 1 5 16v-3a2 2 0 1 1 0-4V6a1.5 1.5 0 0 1 1.5-1.5z',
|
|
68
73
|
};
|
|
69
74
|
|
|
70
75
|
function kindLabel(o: Operation): string {
|
|
@@ -75,9 +80,30 @@ function rowTitle(o: Operation): string {
|
|
|
75
80
|
return o.name || kindLabel(o);
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The words for an app job's output mode, or `''`. ⚠ Only the two WRITING
|
|
85
|
+
* modes get a chip: `none` is the normal case for a job that reports rather
|
|
86
|
+
* than produces (a probe, a notification), and labelling it "no output"
|
|
87
|
+
* would put a badge on almost every row for no information.
|
|
88
|
+
*/
|
|
89
|
+
function outputModeLabel(o: Operation): string {
|
|
90
|
+
if (o.outputMode === 'sibling') return t('plugin.output.sibling');
|
|
91
|
+
if (o.outputMode === 'version') return t('plugin.output.version');
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function outputModeTitle(o: Operation): string {
|
|
96
|
+
if (o.outputMode === 'sibling') return t('plugin.output.sibling_title');
|
|
97
|
+
if (o.outputMode === 'version') return t('plugin.output.version_title');
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
|
|
78
101
|
function statusText(o: Operation): string {
|
|
79
102
|
if (o.status === 'running') {
|
|
80
103
|
if (o.queued) return t('opc.queued');
|
|
104
|
+
// A plugin job says what it is doing in its own words (`job_progress`
|
|
105
|
+
// message); that beats a bare count.
|
|
106
|
+
if (o.message) return o.message;
|
|
81
107
|
if (o.totalBytes && o.totalBytes > 0) {
|
|
82
108
|
return `${formatSize(o.uploadedBytes ?? 0)} / ${formatSize(o.totalBytes)}`;
|
|
83
109
|
}
|
|
@@ -86,11 +112,16 @@ function statusText(o: Operation): string {
|
|
|
86
112
|
}
|
|
87
113
|
return t('opc.status.running');
|
|
88
114
|
}
|
|
89
|
-
if (o.status === 'done') return t('opc.status.done');
|
|
115
|
+
if (o.status === 'done') return o.message || t('opc.status.done');
|
|
90
116
|
if (o.status === 'aborted') return t('opc.status.aborted');
|
|
91
117
|
return o.error || t('opc.status.error');
|
|
92
118
|
}
|
|
93
119
|
|
|
120
|
+
/** A finished job that produced files can be opened from its row. */
|
|
121
|
+
function canOpen(o: Operation): boolean {
|
|
122
|
+
return o.status === 'done' && o.outputs.length > 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
94
125
|
function chipText(o: Operation): string {
|
|
95
126
|
return t(`opc.status.${o.status === 'running' ? 'running' : o.status}`);
|
|
96
127
|
}
|
|
@@ -159,9 +190,24 @@ onBeforeUnmount(() => {
|
|
|
159
190
|
<div class="fe-opc__name" :title="rowTitle(o)">{{ rowTitle(o) }}</div>
|
|
160
191
|
<div class="fe-opc__sub">
|
|
161
192
|
<span class="fe-opc__kind">{{ kindLabel(o) }}</span>
|
|
193
|
+
<template v-if="outputModeLabel(o)">
|
|
194
|
+
<span class="fe-opc__dot" aria-hidden="true">·</span>
|
|
195
|
+
<span class="fe-opc__outmode" :title="outputModeTitle(o)" data-testid="opc-output-mode">
|
|
196
|
+
{{ outputModeLabel(o) }}
|
|
197
|
+
</span>
|
|
198
|
+
</template>
|
|
162
199
|
<span class="fe-opc__dot" aria-hidden="true">·</span>
|
|
163
200
|
<span class="fe-opc__state">{{ statusText(o) }}</span>
|
|
164
201
|
</div>
|
|
202
|
+
<!-- An administrator's second line (lib/errorWords): the raw
|
|
203
|
+
words behind the sentence above. Nobody else gets them. -->
|
|
204
|
+
<div
|
|
205
|
+
v-if="o.status === 'error' && o.errorDetail"
|
|
206
|
+
class="fe-opc__sub fe-opc__sub--detail"
|
|
207
|
+
data-testid="opc-error-detail"
|
|
208
|
+
>
|
|
209
|
+
{{ o.errorDetail }}
|
|
210
|
+
</div>
|
|
165
211
|
<div
|
|
166
212
|
v-if="o.status === 'running' && o.percent !== null"
|
|
167
213
|
class="fe-opc__bar"
|
|
@@ -185,6 +231,13 @@ onBeforeUnmount(() => {
|
|
|
185
231
|
class="fe-opc__act"
|
|
186
232
|
@click="center.cancel(o.key)"
|
|
187
233
|
>{{ t('opc.cancel') }}</button>
|
|
234
|
+
<button
|
|
235
|
+
v-if="canOpen(o)"
|
|
236
|
+
type="button"
|
|
237
|
+
class="fe-opc__act"
|
|
238
|
+
data-testid="opc-open"
|
|
239
|
+
@click="center.open(o.key)"
|
|
240
|
+
>{{ t('opc.open') }}</button>
|
|
188
241
|
<button
|
|
189
242
|
v-if="o.status === 'error' && o.retryable"
|
|
190
243
|
type="button"
|
|
@@ -232,10 +285,33 @@ onBeforeUnmount(() => {
|
|
|
232
285
|
</svg>
|
|
233
286
|
<div class="fe-opc__mid">
|
|
234
287
|
<div class="fe-opc__name" :title="rowTitle(o)">{{ rowTitle(o) }}</div>
|
|
288
|
+
<!-- ⚠ The history row keeps the chip: in `version` mode
|
|
289
|
+
"Open" lands on the file the job started from, and
|
|
290
|
+
without these words that reads as "it did nothing". -->
|
|
291
|
+
<div v-if="outputModeLabel(o)" class="fe-opc__sub">
|
|
292
|
+
<span class="fe-opc__outmode" :title="outputModeTitle(o)" data-testid="opc-output-mode">
|
|
293
|
+
{{ outputModeLabel(o) }}
|
|
294
|
+
</span>
|
|
295
|
+
</div>
|
|
235
296
|
<div v-if="o.status === 'error' && o.error" class="fe-opc__sub fe-opc__sub--err">
|
|
236
297
|
{{ o.error }}
|
|
237
298
|
</div>
|
|
299
|
+
<!-- An administrator's second line: the raw words behind the
|
|
300
|
+
sentence (lib/errorWords). Nobody else is given them. -->
|
|
301
|
+
<div
|
|
302
|
+
v-if="o.status === 'error' && o.errorDetail"
|
|
303
|
+
class="fe-opc__sub fe-opc__sub--detail"
|
|
304
|
+
data-testid="opc-error-detail"
|
|
305
|
+
>
|
|
306
|
+
{{ o.errorDetail }}
|
|
307
|
+
</div>
|
|
238
308
|
</div>
|
|
309
|
+
<button
|
|
310
|
+
v-if="canOpen(o)"
|
|
311
|
+
type="button"
|
|
312
|
+
class="fe-opc__act"
|
|
313
|
+
@click="center.open(o.key)"
|
|
314
|
+
>{{ t('opc.open') }}</button>
|
|
239
315
|
<span class="fe-opc__chip" :class="`is-${o.status}`">{{ chipText(o) }}</span>
|
|
240
316
|
</li>
|
|
241
317
|
</ul>
|
|
@@ -16,26 +16,73 @@
|
|
|
16
16
|
import { watch } from 'vue';
|
|
17
17
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
18
18
|
import type { PendingOp } from '../composables/usePendingOps';
|
|
19
|
-
import type { OperationsStore, OperationStatus } from '../composables/useOperations';
|
|
19
|
+
import type { OperationKind, OperationsStore, OperationStatus } from '../composables/useOperations';
|
|
20
20
|
import { opPercent } from '../lib/opProgress';
|
|
21
|
+
import { opFailure } from '../lib/errorWords';
|
|
22
|
+
import { useLocale } from '../composables/useLocale';
|
|
21
23
|
|
|
22
24
|
const props = defineProps<{
|
|
23
25
|
ops: PendingOp[];
|
|
24
26
|
/** Kept for mount compatibility — strings now render in OperationsCenter. */
|
|
25
27
|
locale: LocaleCode;
|
|
26
28
|
center: OperationsStore;
|
|
29
|
+
/**
|
|
30
|
+
* An app-plugin job's output mode, as the host resolves it (the explorer
|
|
31
|
+
* reads the cached actions list). ⚠ A resolver rather than a field on the
|
|
32
|
+
* row: the ops endpoint does not carry it, so only a caller that holds the
|
|
33
|
+
* actions list can answer — and one that does not simply draws no chip.
|
|
34
|
+
*/
|
|
35
|
+
outputModeOf?: (op: PendingOp) => string | undefined;
|
|
36
|
+
/** May the viewer administer this instance? Then a failure's raw words are
|
|
37
|
+
* shown as a second line (lib/errorWords); nobody else ever sees them. */
|
|
38
|
+
callerAdmin?: boolean;
|
|
27
39
|
}>();
|
|
28
40
|
|
|
41
|
+
const { t } = useLocale(() => props.locale);
|
|
42
|
+
|
|
43
|
+
/** A failed op, said — lib/errorWords `opFailure`, the same call the
|
|
44
|
+
* explorer's toast makes. */
|
|
45
|
+
function failureOf(op: PendingOp) {
|
|
46
|
+
return opFailure(op, t, { callerAdmin: props.callerAdmin === true });
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
const emit = defineEmits<{
|
|
30
50
|
(e: 'dismiss', id: number): void;
|
|
51
|
+
/** A plugin job's Cancel — the explorer posts `opsCancel`. */
|
|
52
|
+
(e: 'cancel', id: number): void;
|
|
53
|
+
/** "Open" on a finished job: reveal this output path in the listing. */
|
|
54
|
+
(e: 'open', id: number, path: string): void;
|
|
31
55
|
}>();
|
|
32
56
|
|
|
33
57
|
function mapStatus(op: PendingOp): OperationStatus {
|
|
34
58
|
if (op.status === 'done') return 'done';
|
|
35
59
|
if (op.status === 'error') return 'error';
|
|
60
|
+
if (op.status === 'cancelled') return 'aborted';
|
|
36
61
|
return 'running'; // pending | running
|
|
37
62
|
}
|
|
38
63
|
|
|
64
|
+
const DRAWN_KINDS: ReadonlySet<string> = new Set(['copy', 'move', 'delete', 'plugin']);
|
|
65
|
+
|
|
66
|
+
/** A queue kind the center can draw. Anything it has no glyph for is shown as
|
|
67
|
+
* a generic job (`plugin`), never folded into delete. */
|
|
68
|
+
function mapKind(op: PendingOp): OperationKind {
|
|
69
|
+
if (op.op_type === 'trash-empty') return 'trash';
|
|
70
|
+
return (DRAWN_KINDS.has(op.op_type) ? op.op_type : 'plugin') as OperationKind;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isPluginJob(op: PendingOp): boolean {
|
|
74
|
+
return op.op_type === 'plugin';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Who may stop a row: a plugin job's starter (the server answers anyone
|
|
78
|
+
* else 403), and an "empty the trash" — which only an administrator
|
|
79
|
+
* starts — for an administrator. */
|
|
80
|
+
function mayCancel(op: PendingOp): boolean {
|
|
81
|
+
if (op.status !== 'pending' && op.status !== 'running') return false;
|
|
82
|
+
if (isPluginJob(op)) return true;
|
|
83
|
+
return op.op_type === 'trash-empty' && props.callerAdmin === true;
|
|
84
|
+
}
|
|
85
|
+
|
|
39
86
|
/* issue #27 — bytes when a cross-storage transfer reports them, no fake 0%
|
|
40
87
|
* otherwise; the rule is shared with the admin tray (lib/opProgress). */
|
|
41
88
|
const percentOf = (op: PendingOp): number | null => opPercent(op);
|
|
@@ -48,19 +95,26 @@ watch(
|
|
|
48
95
|
ops.map((op) => ({
|
|
49
96
|
input: {
|
|
50
97
|
id: op.id,
|
|
51
|
-
kind: op
|
|
52
|
-
|
|
98
|
+
kind: mapKind(op),
|
|
99
|
+
/* A trash empty names no file: its kind is its title. */
|
|
100
|
+
name: op.op_type === 'trash-empty' ? '' : op.label || op.target_path || op.source_dir || '',
|
|
53
101
|
percent: percentOf(op),
|
|
54
102
|
status: mapStatus(op),
|
|
55
|
-
error: op.
|
|
103
|
+
error: op.status === 'error' ? failureOf(op).text : null,
|
|
104
|
+
errorDetail: op.status === 'error' ? (failureOf(op).detail ?? null) : null,
|
|
56
105
|
queued: op.status === 'pending',
|
|
57
106
|
doneCount: op.progress_done,
|
|
58
107
|
totalCount: op.progress_total,
|
|
59
|
-
cancellable:
|
|
108
|
+
cancellable: mayCancel(op),
|
|
60
109
|
retryable: false,
|
|
110
|
+
message: op.message ?? null,
|
|
111
|
+
outputs: (op.outputs ?? []).map((o) => o.path),
|
|
112
|
+
outputMode: (isPluginJob(op) ? props.outputModeOf?.(op) : undefined) ?? null,
|
|
61
113
|
},
|
|
62
114
|
actions: {
|
|
63
115
|
dismiss: () => emit('dismiss', op.id),
|
|
116
|
+
cancel: () => emit('cancel', op.id),
|
|
117
|
+
open: (path: string) => emit('open', op.id, path),
|
|
64
118
|
},
|
|
65
119
|
})),
|
|
66
120
|
);
|
|
@@ -71,7 +71,7 @@ function label(u: PresenceUser): string {
|
|
|
71
71
|
:title="label(u)"
|
|
72
72
|
>
|
|
73
73
|
<PresenceAvatar :user="u" />
|
|
74
|
-
<span class="fx-presence-chip-name">{{ u.name }}</span>
|
|
74
|
+
<span class="fx-presence-chip-name"><bdi>{{ u.name }}</bdi></span>
|
|
75
75
|
<span v-if="u.file" class="fx-presence-chip-file">· {{ u.file }}</span>
|
|
76
76
|
</span>
|
|
77
77
|
</div>
|
|
@@ -131,7 +131,7 @@ function label(u: PresenceUser): string {
|
|
|
131
131
|
justify-content: center;
|
|
132
132
|
width: 1.5rem;
|
|
133
133
|
height: 1.5rem;
|
|
134
|
-
margin-
|
|
134
|
+
margin-inline-start: -0.4rem;
|
|
135
135
|
border-radius: 9999px;
|
|
136
136
|
border: 2px solid var(--fx-presence-ring, #ffffff);
|
|
137
137
|
color: #fff;
|
|
@@ -141,7 +141,7 @@ function label(u: PresenceUser): string {
|
|
|
141
141
|
user-select: none;
|
|
142
142
|
}
|
|
143
143
|
.fx-presence-avatar:first-child {
|
|
144
|
-
margin-
|
|
144
|
+
margin-inline-start: 0;
|
|
145
145
|
}
|
|
146
146
|
/* A photo fills the circle; the identity colour stays as the ring backdrop so
|
|
147
147
|
a picture with transparency still reads as that person. */
|
|
@@ -157,7 +157,7 @@ function label(u: PresenceUser): string {
|
|
|
157
157
|
}
|
|
158
158
|
.fx-presence-dot {
|
|
159
159
|
position: absolute;
|
|
160
|
-
|
|
160
|
+
inset-inline-end: -1px;
|
|
161
161
|
bottom: -1px;
|
|
162
162
|
width: 0.5rem;
|
|
163
163
|
height: 0.5rem;
|
|
@@ -166,7 +166,7 @@ function label(u: PresenceUser): string {
|
|
|
166
166
|
border: 1.5px solid var(--fx-presence-ring, #ffffff);
|
|
167
167
|
}
|
|
168
168
|
.fx-presence-more {
|
|
169
|
-
margin-
|
|
169
|
+
margin-inline-start: 0.15rem;
|
|
170
170
|
font-size: 0.7rem;
|
|
171
171
|
color: rgb(113 113 122); /* zinc-500 */
|
|
172
172
|
}
|
|
@@ -188,7 +188,7 @@ function label(u: PresenceUser): string {
|
|
|
188
188
|
align-items: center;
|
|
189
189
|
gap: 0.35rem;
|
|
190
190
|
flex: 0 0 auto;
|
|
191
|
-
padding: 0.15rem
|
|
191
|
+
padding-block: 0.15rem; padding-inline: 0.2rem 0.5rem;
|
|
192
192
|
border-radius: 9999px;
|
|
193
193
|
background: rgb(244 244 245); /* zinc-100 */
|
|
194
194
|
font-size: 0.75rem;
|
|
@@ -196,7 +196,7 @@ function label(u: PresenceUser): string {
|
|
|
196
196
|
white-space: nowrap;
|
|
197
197
|
}
|
|
198
198
|
.fx-presence-chip .fx-presence-avatar {
|
|
199
|
-
margin-
|
|
199
|
+
margin-inline-start: 0;
|
|
200
200
|
}
|
|
201
201
|
.fx-presence-chip-name {
|
|
202
202
|
font-weight: 500;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ProductVersion — `filex 0.43.0`, as a quiet line (lib/productVersion).
|
|
4
|
+
*
|
|
5
|
+
* The one piece every place that shows the version draws: the admin panel's
|
|
6
|
+
* account menu, the explorer's avatar menu, the user settings dialog. Written
|
|
7
|
+
* once here so the desktop shell (which draws the web app) and an embedding
|
|
8
|
+
* host (which draws its own chrome and can mount this) print the same line.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ `<bdi>`: a Latin name and a dotted number inside a right-to-left menu
|
|
11
|
+
* must keep their own order, and the line still starts on the menu's start
|
|
12
|
+
* side. `dir="ltr"` on the element would flip its alignment as well.
|
|
13
|
+
*
|
|
14
|
+
* ⚠ Its rules are `.fe-version` in styles/base.css, not a `<style>` block
|
|
15
|
+
* here: a component's own styles reach only a bundle that imports it, and
|
|
16
|
+
* the two shipped stylesheets drift (lesson #416).
|
|
17
|
+
*/
|
|
18
|
+
import { computed } from 'vue';
|
|
19
|
+
import { productVersionLine } from '../lib/productVersion';
|
|
20
|
+
|
|
21
|
+
const props = defineProps<{
|
|
22
|
+
/** The server's version string (capabilities). Unknown → nothing drawn. */
|
|
23
|
+
version?: string | null;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const line = computed(() => productVersionLine(props.version));
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<p v-if="line" class="fe-version" data-testid="product-version"><bdi>{{ line }}</bdi></p>
|
|
31
|
+
</template>
|
|
@@ -26,6 +26,7 @@ import { computed, onBeforeUnmount, watch } from 'vue';
|
|
|
26
26
|
import type { FileNode } from '../types/FileNode';
|
|
27
27
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
28
28
|
import { useLocale } from '../composables/useLocale';
|
|
29
|
+
import { inlineKeyStep } from '../lib/direction';
|
|
29
30
|
import { eventMatchesShortcut, shortcutHint } from '../composables/useKeyboardShortcuts';
|
|
30
31
|
import PreviewModal from '../modals/PreviewModal.vue';
|
|
31
32
|
|
|
@@ -70,7 +71,9 @@ const emit = defineEmits<{
|
|
|
70
71
|
(e: 'starred', value: boolean): void;
|
|
71
72
|
}>();
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
// ⚠ RTL: `dir` on the teleported hint, and ← is "next" in a right-to-left
|
|
75
|
+
// interface (the next file's chevron is on the left there).
|
|
76
|
+
const { t, dir } = useLocale(() => props.locale);
|
|
74
77
|
|
|
75
78
|
function inFormControl(target: EventTarget | null): boolean {
|
|
76
79
|
const el = target as HTMLElement | null;
|
|
@@ -114,19 +117,11 @@ function onKeydown(e: KeyboardEvent) {
|
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
emit('nav', 1);
|
|
123
|
-
break;
|
|
124
|
-
case 'ArrowLeft':
|
|
125
|
-
case 'ArrowUp':
|
|
126
|
-
e.preventDefault();
|
|
127
|
-
e.stopPropagation();
|
|
128
|
-
emit('nav', -1);
|
|
129
|
-
break;
|
|
120
|
+
const step = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : inlineKeyStep(e.key, dir.value);
|
|
121
|
+
if (step) {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
emit('nav', step);
|
|
130
125
|
}
|
|
131
126
|
}
|
|
132
127
|
|
|
@@ -200,6 +195,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown, true));
|
|
|
200
195
|
<div
|
|
201
196
|
v-if="open"
|
|
202
197
|
class="fe fe-ql-hint"
|
|
198
|
+
:dir="dir"
|
|
203
199
|
:class="{
|
|
204
200
|
'fe--theme-light': theme === 'light',
|
|
205
201
|
'fe--theme-dark': theme === 'dark',
|