@brftech/filex-core 0.32.0 → 0.33.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/dist/{CsvViewer-DxujFEM3.js → CsvViewer-CqWeV8VO.js} +22 -22
- package/dist/CsvViewer-CqWeV8VO.js.map +1 -0
- package/dist/{DrawioViewer-DCvWnX2t.js → DrawioViewer-BNALOB04.js} +16 -16
- package/dist/DrawioViewer-BNALOB04.js.map +1 -0
- package/dist/filex-core.js +5932 -5841
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +43 -43
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index.d.ts +49 -8
- package/package.json +1 -1
- package/src/FileExplorer.vue +267 -235
- package/src/components/CommandPalette.vue +3 -3
- package/src/components/ConnectionsPanel.vue +2 -1
- package/src/components/GalleryView.vue +3 -3
- package/src/components/GridView.vue +4 -4
- package/src/components/ListView.vue +4 -4
- package/src/components/NFSExportsPanel.vue +2 -1
- package/src/components/PendingOpsTray.vue +1 -1
- package/src/components/PresenceBar.vue +3 -2
- package/src/components/S3KeysPanel.vue +2 -1
- package/src/components/SSHKeysPanel.vue +2 -1
- package/src/components/SecondaryPane.vue +13 -12
- package/src/components/StarButton.vue +2 -1
- package/src/components/TabBar.vue +2 -2
- package/src/components/ThemeGallery.vue +1 -1
- package/src/components/TokensPanel.vue +2 -1
- package/src/components/Toolbar.vue +9 -9
- package/src/composables/useFileApi.ts +4 -2
- package/src/composables/useLocale.ts +1 -1
- package/src/composables/useOperations.ts +3 -3
- package/src/composables/usePendingOps.ts +1 -1
- package/src/composables/useTabs.ts +1 -1
- package/src/composables/useUploadChunked.ts +39 -7
- package/src/index.ts +2 -2
- package/src/lib/themes.ts +7 -7
- package/src/lib/transfer.ts +6 -6
- package/src/locales/en.ts +32 -0
- package/src/locales/index.ts +1 -0
- package/src/locales/resolve.ts +66 -0
- package/src/locales/tr.ts +33 -0
- package/src/modals/ConvertModal.vue +14 -12
- package/src/modals/NewFolderModal.vue +1 -1
- package/src/modals/PermissionsModal.vue +12 -11
- package/src/modals/PreviewModal.vue +9 -9
- package/src/styles/base.css +29 -26
- package/src/styles/variables.css +1 -1
- package/src/types/ExplorerConfig.ts +1 -1
- package/src/types/FileNode.ts +1 -1
- package/src/viewers/CsvViewer.vue +4 -4
- package/src/viewers/DrawioViewer.vue +2 -2
- package/dist/CsvViewer-DxujFEM3.js.map +0 -1
- package/dist/DrawioViewer-DCvWnX2t.js.map +0 -1
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* bul:s3 additions:
|
|
17
17
|
* 4. "Everywhere" group — ≥3-char queries also hit the global files-search
|
|
18
18
|
* endpoint (debounced 250ms, max 8 rows) via the `globalSearch` prop.
|
|
19
|
-
* Content matches get an "
|
|
19
|
+
* Content matches get an "In content" badge + a «»-highlighted snippet
|
|
20
20
|
* rendered as pure text nodes (no innerHTML).
|
|
21
21
|
* 5. Saved searches — localStorage-backed (`filex.saved-searches`, max 10);
|
|
22
22
|
* "save current query" command + per-row delete.
|
|
@@ -140,7 +140,7 @@ const commandItems = computed<PaletteItem[]>(() => {
|
|
|
140
140
|
const defs: Array<{ command: string; label: string; icon: string; enabled: boolean }> = [
|
|
141
141
|
{ command: 'new-folder', label: t('toolbar.new_folder'), icon: '📁', enabled: props.canWrite !== false },
|
|
142
142
|
{ command: 'upload', label: t('toolbar.upload'), icon: '⬆', enabled: props.canWrite !== false },
|
|
143
|
-
{ command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? '▦' : props.viewMode === 'grid' ? '▣' : '☰', enabled: true /* wiring:d2 —
|
|
143
|
+
{ command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? '▦' : props.viewMode === 'grid' ? '▣' : '☰', enabled: true /* wiring:d2 — icon of the NEXT mode (list→grid→gallery→list) */ },
|
|
144
144
|
{ command: 'open-trash', label: t('cmd.trash'), icon: '🗑', enabled: true },
|
|
145
145
|
{ command: 'refresh', label: t('toolbar.refresh'), icon: '⟳', enabled: true },
|
|
146
146
|
{ command: 'go-up', label: t('toolbar.go_up'), icon: '↑', enabled: props.canGoUp !== false },
|
|
@@ -148,7 +148,7 @@ const commandItems = computed<PaletteItem[]>(() => {
|
|
|
148
148
|
{ command: 'open-theme', label: t('theme.menu'), icon: '🎨', enabled: true },
|
|
149
149
|
{ command: 'open-shortcut-settings', label: t('shortcuts.settings.menu'), icon: '⌨', enabled: true },
|
|
150
150
|
{ command: 'start-tour', label: t('tour.restart'), icon: '🎓', enabled: true },
|
|
151
|
-
/* wiring:d1 —
|
|
151
|
+
/* wiring:d1 — tabs + split */
|
|
152
152
|
{ command: 'tab-new', label: t('cmd.tab_new'), icon: '⧉', enabled: true },
|
|
153
153
|
{ command: 'split-toggle', label: t('cmd.split_toggle'), icon: '◫', enabled: props.splitEnabled !== false },
|
|
154
154
|
];
|
|
@@ -40,6 +40,7 @@ import S3KeysPanel from './S3KeysPanel.vue';
|
|
|
40
40
|
import SSHKeysPanel from './SSHKeysPanel.vue';
|
|
41
41
|
import NFSExportsPanel from './NFSExportsPanel.vue';
|
|
42
42
|
import TokensPanel from './TokensPanel.vue';
|
|
43
|
+
import { resolveLocale } from '../locales/resolve';
|
|
43
44
|
|
|
44
45
|
const props = defineProps<{
|
|
45
46
|
config: ExplorerConfig;
|
|
@@ -56,7 +57,7 @@ const emit = defineEmits<{
|
|
|
56
57
|
(e: 'error', err: { message: string }): void;
|
|
57
58
|
}>();
|
|
58
59
|
|
|
59
|
-
const locale = computed<LocaleCode>(() => props.config.locale
|
|
60
|
+
const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
|
|
60
61
|
const { t } = useLocale(locale);
|
|
61
62
|
|
|
62
63
|
// Destructured on purpose: Vue only auto-unwraps refs that are top-level in
|
|
@@ -43,8 +43,8 @@ const props = defineProps<{
|
|
|
43
43
|
* leaves that view out — a shared app token has no single person behind it,
|
|
44
44
|
* so "your starred files" is one list shown to strangers — offering to star
|
|
45
45
|
* something is offering to write into that same shared list. Owner's call,
|
|
46
|
-
* 2026-09-05
|
|
47
|
-
*
|
|
46
|
+
* 2026-09-05, verbatim (translated from Turkish): "if the starred place is
|
|
47
|
+
* not visible, then star/unstar should not be visible either".
|
|
48
48
|
*/
|
|
49
49
|
starEnabled?: boolean;
|
|
50
50
|
apiBase?: string;
|
|
@@ -159,7 +159,7 @@ function parentDir(path: string): string {
|
|
|
159
159
|
function specialEmojiFor(n: FileNode): string | null {
|
|
160
160
|
if (n.basename === '.trash') return '🗑';
|
|
161
161
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
162
|
-
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 —
|
|
162
|
+
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — encrypted-folder badge */
|
|
163
163
|
return null;
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -41,8 +41,8 @@ const props = defineProps<{
|
|
|
41
41
|
* leaves that view out — a shared app token has no single person behind it,
|
|
42
42
|
* so "your starred files" is one list shown to strangers — offering to star
|
|
43
43
|
* something is offering to write into that same shared list. Owner's call,
|
|
44
|
-
* 2026-09-05
|
|
45
|
-
*
|
|
44
|
+
* 2026-09-05, verbatim (translated from Turkish): "if the starred place is
|
|
45
|
+
* not visible, then star/unstar should not be visible either".
|
|
46
46
|
*/
|
|
47
47
|
starEnabled?: boolean;
|
|
48
48
|
apiBase?: string;
|
|
@@ -197,7 +197,7 @@ function keepGlyph(b: 'kept' | 'syncing' | 'cloud' | 'partial'): string {
|
|
|
197
197
|
function specialEmojiFor(n: FileNode): string | null {
|
|
198
198
|
if (n.basename === '.trash') return '🗑';
|
|
199
199
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
200
|
-
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 —
|
|
200
|
+
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — encrypted-folder badge */
|
|
201
201
|
return null;
|
|
202
202
|
}
|
|
203
203
|
|
|
@@ -246,7 +246,7 @@ function snippetTitle(snippet: string): string {
|
|
|
246
246
|
role="option"
|
|
247
247
|
:aria-selected="isSelected(n) ? 'true' : 'false'"
|
|
248
248
|
:aria-label="nodeDisplayName(n) /* wiring:c4 */"
|
|
249
|
-
:data-fe-path="n.path /* wiring:d1 —
|
|
249
|
+
:data-fe-path="n.path /* wiring:d1 — middle-click open-in-new-tab delegation */"
|
|
250
250
|
draggable="true"
|
|
251
251
|
@click="onClick(n, $event)"
|
|
252
252
|
@dblclick="onDbl(n)"
|
|
@@ -38,8 +38,8 @@ const props = defineProps<{
|
|
|
38
38
|
* leaves that view out — a shared app token has no single person behind it,
|
|
39
39
|
* so "your starred files" is one list shown to strangers — offering to star
|
|
40
40
|
* something is offering to write into that same shared list. Owner's call,
|
|
41
|
-
* 2026-09-05
|
|
42
|
-
*
|
|
41
|
+
* 2026-09-05, verbatim (translated from Turkish): "if the starred place is
|
|
42
|
+
* not visible, then star/unstar should not be visible either".
|
|
43
43
|
*/
|
|
44
44
|
starEnabled?: boolean;
|
|
45
45
|
/** Backend base URL + auth header builder forwarded to StarButton
|
|
@@ -154,7 +154,7 @@ function cancelPress() {
|
|
|
154
154
|
function specialEmojiFor(n: FileNode): string | null {
|
|
155
155
|
if (n.basename === '.trash') return '🗑';
|
|
156
156
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
157
|
-
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 —
|
|
157
|
+
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — encrypted-folder badge */
|
|
158
158
|
return null;
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -384,7 +384,7 @@ const segments = computed<Segment[]>(() => {
|
|
|
384
384
|
tabindex="0"
|
|
385
385
|
:aria-selected="isSelected(n) ? 'true' : 'false'"
|
|
386
386
|
:aria-label="nodeDisplayName(n) /* wiring:c4 */"
|
|
387
|
-
:data-fe-path="n.path /* wiring:d1 —
|
|
387
|
+
:data-fe-path="n.path /* wiring:d1 — middle-click open-in-new-tab delegation */"
|
|
388
388
|
draggable="true"
|
|
389
389
|
@click="onRowClick(n, $event)"
|
|
390
390
|
@dblclick="onRowDbl(n)"
|
|
@@ -17,6 +17,7 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
|
|
|
17
17
|
import type { NFSExport } from '../types/NFSExports';
|
|
18
18
|
import { useLocale } from '../composables/useLocale';
|
|
19
19
|
import { useNFSExports } from '../composables/useNFSExports';
|
|
20
|
+
import { resolveLocale } from '../locales/resolve';
|
|
20
21
|
|
|
21
22
|
const props = defineProps<{
|
|
22
23
|
config: ExplorerConfig;
|
|
@@ -28,7 +29,7 @@ const emit = defineEmits<{
|
|
|
28
29
|
(e: 'active', v: { host: string; port: number; enabled: boolean; path?: string; readOnly: boolean }): void;
|
|
29
30
|
}>();
|
|
30
31
|
|
|
31
|
-
const locale = computed<LocaleCode>(() => props.config.locale
|
|
32
|
+
const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
|
|
32
33
|
const { t } = useLocale(locale);
|
|
33
34
|
|
|
34
35
|
const {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Historically a bottom-left toast list for queued copy/move/delete. Since
|
|
6
6
|
* the unified operations center it renders NOTHING: it reconciles the
|
|
7
7
|
* usePendingOps rows into the shared useOperations store; OperationsCenter
|
|
8
|
-
* draws them. Terminal-state lingering ("
|
|
8
|
+
* draws them. Terminal-state lingering ("done" flash → history) and sticky
|
|
9
9
|
* errors are handled by the store — note usePendingOps sweeps terminal rows
|
|
10
10
|
* after its RETAIN window, which the store treats as "move to history"
|
|
11
11
|
* (errors stay pinned until the user dismisses them).
|
|
@@ -11,6 +11,7 @@ import type { PresenceUser } from '../lib/realtime';
|
|
|
11
11
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
12
12
|
import { useLocale } from '../composables/useLocale';
|
|
13
13
|
import PresenceAvatar from './PresenceAvatar.vue';
|
|
14
|
+
import { resolveLocale } from '../locales/resolve';
|
|
14
15
|
|
|
15
16
|
const props = defineProps<{
|
|
16
17
|
users: PresenceUser[];
|
|
@@ -18,7 +19,7 @@ const props = defineProps<{
|
|
|
18
19
|
locale?: LocaleCode;
|
|
19
20
|
}>();
|
|
20
21
|
|
|
21
|
-
const { t } = useLocale(() => props.locale
|
|
22
|
+
const { t } = useLocale(() => resolveLocale(props.locale));
|
|
22
23
|
|
|
23
24
|
const others = computed(() =>
|
|
24
25
|
(props.users ?? []).filter((u) => props.selfId == null || u.id !== props.selfId),
|
|
@@ -87,7 +88,7 @@ function label(u: PresenceUser): string {
|
|
|
87
88
|
<span v-if="others.length > 5" class="fx-presence-more">+{{ others.length - 5 }}</span>
|
|
88
89
|
</div>
|
|
89
90
|
<span class="fx-presence-text" :title="others.map(label).join(', ')">
|
|
90
|
-
{{ others.length === 1 ? others[0].name :
|
|
91
|
+
{{ others.length === 1 ? others[0].name : t('presence.others', { n: others.length }) }}
|
|
91
92
|
<template v-if="others.length === 1 && others[0].file">
|
|
92
93
|
· <span class="fx-presence-file">{{ others[0].file }}</span>
|
|
93
94
|
</template>
|
|
@@ -22,6 +22,7 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
|
|
|
22
22
|
import type { S3AccessKey } from '../types/S3Keys';
|
|
23
23
|
import { useLocale } from '../composables/useLocale';
|
|
24
24
|
import { useS3Keys } from '../composables/useS3Keys';
|
|
25
|
+
import { resolveLocale } from '../locales/resolve';
|
|
25
26
|
|
|
26
27
|
const props = defineProps<{
|
|
27
28
|
config: ExplorerConfig;
|
|
@@ -34,7 +35,7 @@ const emit = defineEmits<{
|
|
|
34
35
|
(e: 'active', v: { accessKeyID: string; secret?: string; endpoint: string; pathStyle: boolean }): void;
|
|
35
36
|
}>();
|
|
36
37
|
|
|
37
|
-
const locale = computed<LocaleCode>(() => props.config.locale
|
|
38
|
+
const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
|
|
38
39
|
const { t } = useLocale(locale);
|
|
39
40
|
|
|
40
41
|
const {
|
|
@@ -18,6 +18,7 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
|
|
|
18
18
|
import type { FTPSFacts, SSHPublicKey } from '../types/SSHKeys';
|
|
19
19
|
import { useLocale } from '../composables/useLocale';
|
|
20
20
|
import { useSSHKeys } from '../composables/useSSHKeys';
|
|
21
|
+
import { resolveLocale } from '../locales/resolve';
|
|
21
22
|
|
|
22
23
|
const props = defineProps<{
|
|
23
24
|
config: ExplorerConfig;
|
|
@@ -47,7 +48,7 @@ const emit = defineEmits<{
|
|
|
47
48
|
}): void;
|
|
48
49
|
}>();
|
|
49
50
|
|
|
50
|
-
const locale = computed<LocaleCode>(() => props.config.locale
|
|
51
|
+
const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
|
|
51
52
|
const { t } = useLocale(locale);
|
|
52
53
|
|
|
53
54
|
const { keys, connection, loading, error, canAdd, hasUsableKey, load, add, setDisabled, remove } =
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* SecondaryPane — wiring:d1
|
|
3
|
+
* SecondaryPane — wiring:d1 the right-hand (secondary) pane of the split view.
|
|
4
4
|
*
|
|
5
5
|
* Deliberately LIGHT: its own path + its own listing — fetched through
|
|
6
6
|
* the SAME FileApi instance the host already constructed (`api.index`,
|
|
@@ -79,11 +79,12 @@ const emit = defineEmits<{
|
|
|
79
79
|
(e: 'close'): void;
|
|
80
80
|
(e: 'open-tab', path: string): void;
|
|
81
81
|
(e: 'transfer', p: { sources: string[]; targetWire: string; originWire?: string }): void;
|
|
82
|
-
/* ui-fix —
|
|
83
|
-
* node=null →
|
|
82
|
+
/* ui-fix — right-click inside the side pane: the menu is opened by the main
|
|
83
|
+
* component (FileExplorer). node=null → right-click on empty space (nothing
|
|
84
|
+
* selected, so "Paste" only). */
|
|
84
85
|
(e: 'context', node: FileNode | null, ev: MouseEvent): void;
|
|
85
|
-
/* ui-fix —
|
|
86
|
-
* (
|
|
86
|
+
/* ui-fix — when the trash row is opened: the trash view belongs to the main
|
|
87
|
+
* panel (it carries the restore actions), so the host opens it via loadTrash(). */
|
|
87
88
|
(e: 'open-trash'): void;
|
|
88
89
|
}>();
|
|
89
90
|
|
|
@@ -144,7 +145,7 @@ async function loadPane(target?: string): Promise<void> {
|
|
|
144
145
|
void loadPane(props.initialPath ?? '');
|
|
145
146
|
|
|
146
147
|
// ------------------------------------------------------------------
|
|
147
|
-
// Breadcrumb
|
|
148
|
+
// Breadcrumb
|
|
148
149
|
// ------------------------------------------------------------------
|
|
149
150
|
|
|
150
151
|
interface Crumb {
|
|
@@ -204,12 +205,12 @@ function onViewContext(n: FileNode, ev: MouseEvent) {
|
|
|
204
205
|
emit('context', n, ev);
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
/* ui-fix —
|
|
208
|
-
* →
|
|
209
|
-
*
|
|
208
|
+
/* ui-fix — right-click on empty space (the background): a selection-less menu
|
|
209
|
+
* ("Paste" only) → pasting files into an empty / into the current folder. The
|
|
210
|
+
* row menu calls stopProp, so this only fires on genuinely empty space. */
|
|
210
211
|
function onBgContext(ev: MouseEvent) {
|
|
211
212
|
ev.preventDefault();
|
|
212
|
-
ev.stopPropagation(); //
|
|
213
|
+
ev.stopPropagation(); // don't bubble into the canvas menu on the FileExplorer root
|
|
213
214
|
emit('activate');
|
|
214
215
|
selected.value = new Set();
|
|
215
216
|
emit('context', null, ev);
|
|
@@ -399,8 +400,8 @@ defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
|
|
|
399
400
|
<div v-else-if="files.length === 0" class="fe-split__state">
|
|
400
401
|
{{ t('empty.folder') }}
|
|
401
402
|
</div>
|
|
402
|
-
<!-- ui-fix —
|
|
403
|
-
|
|
403
|
+
<!-- ui-fix — the same view components (list/grid/gallery), driven by the
|
|
404
|
+
pane's own viewMode; the old flat fe-split__list is gone. -->
|
|
404
405
|
<ListView
|
|
405
406
|
v-else-if="(viewMode ?? 'list') === 'list'"
|
|
406
407
|
:files="files"
|
|
@@ -10,6 +10,7 @@ import { ref, watch, computed } from 'vue';
|
|
|
10
10
|
import { setNodeStarred } from '../lib/star';
|
|
11
11
|
import { useLocale } from '../composables/useLocale';
|
|
12
12
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
13
|
+
import { resolveLocale } from '../locales/resolve';
|
|
13
14
|
|
|
14
15
|
const props = defineProps<{
|
|
15
16
|
starred: boolean;
|
|
@@ -45,7 +46,7 @@ const emit = defineEmits<{
|
|
|
45
46
|
const local = ref(props.starred);
|
|
46
47
|
watch(() => props.starred, (v) => { local.value = v; });
|
|
47
48
|
|
|
48
|
-
const { t } = useLocale(() => props.locale
|
|
49
|
+
const { t } = useLocale(() => resolveLocale(props.locale));
|
|
49
50
|
const label = computed(() => t(local.value ? 'ctx.unstar' : 'ctx.star'));
|
|
50
51
|
|
|
51
52
|
async function toggle() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* TabBar — wiring:d1
|
|
3
|
+
* TabBar — wiring:d1 the tab strip.
|
|
4
4
|
*
|
|
5
5
|
* Thin strip that sits ABOVE the toolbar. The host renders it only when
|
|
6
6
|
* 2+ tabs exist (embed pixel-parity: a single tab shows nothing at all).
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Interactions:
|
|
10
10
|
* click tab → select
|
|
11
|
-
*
|
|
11
|
+
* ×/middle-click → close
|
|
12
12
|
* + → new tab (clone of the current location)
|
|
13
13
|
* drag → reorder (HTML5 DnD; live-swap while dragging)
|
|
14
14
|
* ⫿ (split) → toggle the active tab's secondary pane
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* toolbar chips + sample rows) painted with that theme's tokens for the
|
|
7
7
|
* CURRENTLY resolved light/dark mode — so the card shows exactly what
|
|
8
8
|
* you'd get. Click applies instantly (live preview behind the modal) and
|
|
9
|
-
* persists; the selected card carries a ✓. "
|
|
9
|
+
* persists; the selected card carries a ✓. "Reset to default" resets.
|
|
10
10
|
*
|
|
11
11
|
* Presentational only — selection state + persistence live in
|
|
12
12
|
* lib/themes.ts (shared across instances); the parent forwards `select`.
|
|
@@ -30,6 +30,7 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
|
|
|
30
30
|
import type { ApiToken } from '../types/Tokens';
|
|
31
31
|
import { useLocale } from '../composables/useLocale';
|
|
32
32
|
import { useTokens } from '../composables/useTokens';
|
|
33
|
+
import { resolveLocale } from '../locales/resolve';
|
|
33
34
|
|
|
34
35
|
const props = defineProps<{
|
|
35
36
|
config: ExplorerConfig;
|
|
@@ -50,7 +51,7 @@ const emit = defineEmits<{
|
|
|
50
51
|
(e: 'active', v: { hasToken: boolean }): void;
|
|
51
52
|
}>();
|
|
52
53
|
|
|
53
|
-
const locale = computed<LocaleCode>(() => props.config.locale
|
|
54
|
+
const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
|
|
54
55
|
const { t } = useLocale(locale);
|
|
55
56
|
|
|
56
57
|
const { tokens, loading, error, canMint, revealed, load, create, remove, dismiss } = useTokens(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Toolbar — selection-aware action row.
|
|
4
4
|
*
|
|
5
5
|
* Layout:
|
|
6
|
-
* - No selection → [📁
|
|
6
|
+
* - No selection → [📁 New Folder] [search | ⬆ ↻] [☰ ▦]
|
|
7
7
|
* - 1 file selected → [👁 ⬇ 🔗 ✎ 🗑] [search | ⬆ ↻] [☰ ▦]
|
|
8
8
|
* - 1 folder selected → [↗ ✎ 🗑] [search | ⬆ ↻] [☰ ▦]
|
|
9
9
|
* - Multi selection → [✂ ❐ 🗑] [search | ⬆ ↻] [☰ ▦]
|
|
@@ -334,8 +334,8 @@ const utilityActions = computed<ContextAction[]>(() => {
|
|
|
334
334
|
: t('toolbar.density.compact'),
|
|
335
335
|
icon: '⇅',
|
|
336
336
|
});
|
|
337
|
-
/* wiring:d2 —
|
|
338
|
-
(list/grid/gallery);
|
|
337
|
+
/* wiring:d2 — narrow-mode ⋯ menu: BOTH inactive views are listed
|
|
338
|
+
(list/grid/gallery); the old single toggle fell short with three modes. */
|
|
339
339
|
if (props.viewMode !== 'list' && offersView('list')) list.push({ key: 'view-list', label: t('toolbar.view.list'), icon: '☰' });
|
|
340
340
|
if (props.viewMode !== 'grid' && offersView('grid')) list.push({ key: 'view-grid', label: t('toolbar.view.grid'), icon: '▦' });
|
|
341
341
|
if (props.viewMode !== 'gallery' && offersView('gallery')) list.push({ key: 'view-gallery', label: t('toolbar.view.gallery'), icon: '▣' });
|
|
@@ -347,7 +347,7 @@ const utilityActions = computed<ContextAction[]>(() => {
|
|
|
347
347
|
}
|
|
348
348
|
/* koru:k1 — inspector toggle also reachable from the narrow overflow menu */
|
|
349
349
|
list.push({ key: 'inspector', label: t('toolbar.inspector'), icon: 'ℹ' });
|
|
350
|
-
/* wiring:c1 —
|
|
350
|
+
/* wiring:c1 — the theme gallery is opened from the ⋯ menu in narrow mode too */
|
|
351
351
|
list.push({ key: 'theme', label: t('theme.menu'), icon: '🎨' });
|
|
352
352
|
list.push({ key: 'shortcut-settings', label: t('shortcuts.settings.menu'), icon: '⌨' }) /* wiring:c2 */;
|
|
353
353
|
list.push({ key: 'tour', label: t('tour.restart'), icon: '🎓' }); /* wiring:c4 */
|
|
@@ -539,8 +539,8 @@ function onMoreSelect(a: ContextAction) {
|
|
|
539
539
|
<span class="fe-btn__label">{{ a.label }}</span>
|
|
540
540
|
</button>
|
|
541
541
|
|
|
542
|
-
<!-- ui-fix —
|
|
543
|
-
|
|
542
|
+
<!-- ui-fix — actions that don't fit fold into the ⋯ menu so the row stays
|
|
543
|
+
a single line (search/view controls no longer drop to a second row). -->
|
|
544
544
|
<button
|
|
545
545
|
v-if="overflowToolbarItems.length > 0"
|
|
546
546
|
ref="wideMoreBtnEl"
|
|
@@ -554,8 +554,8 @@ function onMoreSelect(a: ContextAction) {
|
|
|
554
554
|
<span class="fe-icon">⋯</span>
|
|
555
555
|
</button>
|
|
556
556
|
|
|
557
|
-
<!--
|
|
558
|
-
|
|
557
|
+
<!-- Invisible measuring strip: EVERY action is always rendered here, so
|
|
558
|
+
the fold calculation runs off real measured widths. -->
|
|
559
559
|
<div ref="measureEl" class="fe-toolbar__measure" aria-hidden="true">
|
|
560
560
|
<button
|
|
561
561
|
v-for="a in toolbarItems"
|
|
@@ -969,7 +969,7 @@ function onMoreSelect(a: ContextAction) {
|
|
|
969
969
|
/>
|
|
970
970
|
</template>
|
|
971
971
|
|
|
972
|
-
<!-- ui-fix —
|
|
972
|
+
<!-- ui-fix — ⋯ menu for the actions folded away in wide mode -->
|
|
973
973
|
<ContextMenu
|
|
974
974
|
v-if="!narrow"
|
|
975
975
|
ref="wideMoreRef"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import type { ExplorerConfig, AuthConfig, EndpointMap } from '../types/ExplorerConfig';
|
|
25
|
+
import { resolveLocale } from '../locales/resolve';
|
|
25
26
|
import type {
|
|
26
27
|
FileNode,
|
|
27
28
|
ShareInfo,
|
|
@@ -336,9 +337,10 @@ export function useFileApi(config: ExplorerConfig) {
|
|
|
336
337
|
|
|
337
338
|
// Map an HTTP status to a short, human-readable message in the explorer's
|
|
338
339
|
// locale. The raw JSON body is attached as `.detail` for debugging but never
|
|
339
|
-
// shown in the toast (Ada: "
|
|
340
|
+
// shown in the toast (Ada, translated from Turkish: "when it gives a 404/403
|
|
341
|
+
// or whatever, I see raw json").
|
|
340
342
|
function statusMessage(status: number): string {
|
|
341
|
-
const tr = (config.locale
|
|
343
|
+
const tr = resolveLocale(config.locale) !== 'en';
|
|
342
344
|
const m: Record<number, [string, string]> = {
|
|
343
345
|
400: ['Geçersiz istek', 'Bad request'],
|
|
344
346
|
401: ['Oturum gerekli, tekrar giriş yapın', 'Sign-in required'],
|
|
@@ -12,7 +12,7 @@ import { messages } from '../locales';
|
|
|
12
12
|
export function useLocale(localeRef: Ref<LocaleCode> | (() => LocaleCode)) {
|
|
13
13
|
const lookup = computed(() => {
|
|
14
14
|
const code = typeof localeRef === 'function' ? localeRef() : localeRef.value;
|
|
15
|
-
return messages[code] ?? messages.
|
|
15
|
+
return messages[code] ?? messages.en;
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
function t(key: string, vars: Record<string, string | number> = {}): string {
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* are registered per row so the panel can route user intent back to the
|
|
15
15
|
* owning surface without the store knowing any transport details.
|
|
16
16
|
*
|
|
17
|
-
* Lifecycle rules (per the
|
|
17
|
+
* Lifecycle rules (per the Operations Center contract):
|
|
18
18
|
* - running rows live in `active`;
|
|
19
|
-
* - done/aborted rows linger in `active` for DONE_LINGER_MS ("
|
|
19
|
+
* - done/aborted rows linger in `active` for DONE_LINGER_MS ("done"
|
|
20
20
|
* flash) then move to `history` (session-only, capped);
|
|
21
21
|
* - error rows are STICKY: they stay in `active` until the user
|
|
22
22
|
* dismisses or retries them — even after the source list swept them
|
|
@@ -39,7 +39,7 @@ export interface OperationInput {
|
|
|
39
39
|
percent: number | null;
|
|
40
40
|
status: OperationStatus;
|
|
41
41
|
error?: string | null;
|
|
42
|
-
/** Queue op accepted but not started yet ("
|
|
42
|
+
/** Queue op accepted but not started yet ("Queued"). */
|
|
43
43
|
queued?: boolean;
|
|
44
44
|
/** Progress counters for queue ops (3/5 items). */
|
|
45
45
|
doneCount?: number;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Backend POST /files/{copy,move,delete} returns `{op}` immediately
|
|
5
5
|
* (status='pending') and the actual S3 work runs in a worker. The
|
|
6
6
|
* client polls `/files/ops` every 2s while there's anything still
|
|
7
|
-
* pending or running so the user gets live feedback ("3/5
|
|
7
|
+
* pending or running so the user gets live feedback ("3/5 copied").
|
|
8
8
|
*
|
|
9
9
|
* Polling is reference-counted: callers `register(op)` after starting a
|
|
10
10
|
* new op; an interval ticks while the registered set has any non-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useTabs — wiring:d1
|
|
2
|
+
* useTabs — wiring:d1 tabs (the workspace tab strip).
|
|
3
3
|
*
|
|
4
4
|
* A LAYER ABOVE FileExplorer's location state, never a replacement for
|
|
5
5
|
* it: each tab is a location snapshot `{ id, path, viewMode, split }`.
|
|
@@ -85,10 +85,17 @@ export interface UploadOptions {
|
|
|
85
85
|
onDone?: (job: UploadJob, result: UploadResult) => void;
|
|
86
86
|
onError?: (job: UploadJob, err: Error) => void;
|
|
87
87
|
/**
|
|
88
|
-
* Wait for the backend transfer before resolving.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
88
|
+
* Wait for the backend transfer before resolving. Defaults to TRUE, because
|
|
89
|
+
* "the server has the bytes" and "the storage has the bytes" are different
|
|
90
|
+
* claims and only the second one is an upload.
|
|
91
|
+
*
|
|
92
|
+
* ⚠ It used to default to false and no caller ever set it, so the job went
|
|
93
|
+
* straight to `done` on the 202 — a transfer that failed afterwards in the
|
|
94
|
+
* ops worker left the user looking at a finished upload for a file that was
|
|
95
|
+
* never stored (issue #16: every file over the chunk threshold, on every S3
|
|
96
|
+
* backend). The `transferring` phase is short for a healthy storage and is
|
|
97
|
+
* the only place a failure can still be shown, so it is the default; pass
|
|
98
|
+
* false to opt out where the wait is genuinely unwanted.
|
|
92
99
|
*/
|
|
93
100
|
waitForTransfer?: boolean;
|
|
94
101
|
}
|
|
@@ -389,7 +396,7 @@ export function useUploadChunked(
|
|
|
389
396
|
job.opId = result?.op_id;
|
|
390
397
|
job.nodeId = result?.node_id;
|
|
391
398
|
|
|
392
|
-
if (opts.waitForTransfer && result?.op_id && api.endpoints.opsShow) {
|
|
399
|
+
if (opts.waitForTransfer !== false && result?.op_id && api.endpoints.opsShow) {
|
|
393
400
|
job.status = 'transferring';
|
|
394
401
|
report();
|
|
395
402
|
await waitForOp(result.op_id);
|
|
@@ -484,17 +491,31 @@ export function useUploadChunked(
|
|
|
484
491
|
if (!tmpl) return;
|
|
485
492
|
const url = tmpl.replace('{id}', String(opId));
|
|
486
493
|
let delay = 200;
|
|
494
|
+
// ⚠ A tray read that keeps failing must not park the upload in
|
|
495
|
+
// `transferring` for ever. One hiccup is not a verdict, but a row we
|
|
496
|
+
// cannot read at all (purged, or the endpoint is gone) is unknowable, so
|
|
497
|
+
// after a bounded run of failures the wait ends and the job settles the
|
|
498
|
+
// way it did before the wait existed — optimistic, but not hung.
|
|
499
|
+
let misses = 0;
|
|
487
500
|
for (;;) {
|
|
488
501
|
if (cancelled) throw new DOMException('Aborted by user', 'AbortError');
|
|
489
502
|
try {
|
|
490
503
|
const op = await api.jsonFetch<{ status?: string; error?: string }>(url);
|
|
491
504
|
if (op?.status === 'ok') return;
|
|
492
505
|
if (op?.status === 'failed' || op?.status === 'partial') {
|
|
493
|
-
throw new
|
|
506
|
+
throw new TransferFailedError(op.error || 'transfer failed');
|
|
494
507
|
}
|
|
508
|
+
misses = 0;
|
|
495
509
|
} catch (err) {
|
|
496
|
-
|
|
510
|
+
// ⚠ The verdict is told apart from the hiccup by TYPE, not by
|
|
511
|
+
// message. It used to be `message === 'transfer failed'`, which only
|
|
512
|
+
// matched when the server sent NO error text: the moment it sent a
|
|
513
|
+
// real one — which is every interesting failure — the throw above was
|
|
514
|
+
// swallowed here and the poll span for ever. Harmless while nothing
|
|
515
|
+
// set waitForTransfer; a hang the moment anything did.
|
|
516
|
+
if (err instanceof TransferFailedError || (err as Error).name === 'AbortError') throw err;
|
|
497
517
|
/* a hiccup reading the tray is not a failed transfer */
|
|
518
|
+
if (++misses >= MAX_OP_POLL_MISSES) return;
|
|
498
519
|
}
|
|
499
520
|
await sleep(delay);
|
|
500
521
|
delay = Math.min(delay * 2, 2000);
|
|
@@ -514,6 +535,17 @@ export function useUploadChunked(
|
|
|
514
535
|
};
|
|
515
536
|
}
|
|
516
537
|
|
|
538
|
+
/** How many consecutive unreadable op polls end the transfer wait (~7s). */
|
|
539
|
+
const MAX_OP_POLL_MISSES = 6;
|
|
540
|
+
|
|
541
|
+
/** The backend said the transfer failed — as opposed to "I could not ask". */
|
|
542
|
+
class TransferFailedError extends Error {
|
|
543
|
+
constructor(message: string) {
|
|
544
|
+
super(message);
|
|
545
|
+
this.name = 'TransferFailedError';
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
517
549
|
function sleep(ms: number): Promise<void> {
|
|
518
550
|
return new Promise((r) => setTimeout(r, ms));
|
|
519
551
|
}
|
package/src/index.ts
CHANGED
|
@@ -98,7 +98,7 @@ export {
|
|
|
98
98
|
} from './composables/useMonacoLoader';
|
|
99
99
|
|
|
100
100
|
// ——— Locale catalogue (consumers may merge their own keys) ———
|
|
101
|
-
export { messages, tr, en } from './locales';
|
|
101
|
+
export { messages, tr, en, resolveLocale, detectLocale } from './locales';
|
|
102
102
|
|
|
103
103
|
/* wiring:c1 — theme registry + gallery (hosts can list/apply themes programmatically) */
|
|
104
104
|
export {
|
|
@@ -150,7 +150,7 @@ export type { TabState, TabSplit, TabsApi } from './composables/useTabs';
|
|
|
150
150
|
export { default as TabBar } from './components/TabBar.vue';
|
|
151
151
|
export { default as SecondaryPane } from './components/SecondaryPane.vue';
|
|
152
152
|
/* /wiring:d1 */
|
|
153
|
-
/* wiring:e2 —
|
|
153
|
+
/* wiring:e2 — end-to-end encrypted folders (WebCrypto; docs/E2E-ENCRYPTION.md) */
|
|
154
154
|
export {
|
|
155
155
|
E2E_MARKER_NAME,
|
|
156
156
|
E2E_MAGIC,
|