@brftech/filex-core 0.6.0 → 0.7.1

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.
Files changed (47) hide show
  1. package/dist/{ArchiveViewer-EGgNqLPE.js → ArchiveViewer-Qi4L2jj9.js} +2 -2
  2. package/dist/{ArchiveViewer-EGgNqLPE.js.map → ArchiveViewer-Qi4L2jj9.js.map} +1 -1
  3. package/dist/{CsvViewer-BvriJ42M.js → CsvViewer-IOIrJDsJ.js} +2 -2
  4. package/dist/{CsvViewer-BvriJ42M.js.map → CsvViewer-IOIrJDsJ.js.map} +1 -1
  5. package/dist/{DrawioViewer-B_bSPsWX.js → DrawioViewer-D0KtUQBO.js} +2 -2
  6. package/dist/{DrawioViewer-B_bSPsWX.js.map → DrawioViewer-D0KtUQBO.js.map} +1 -1
  7. package/dist/{EpubViewer-hl68h4gG.js → EpubViewer-DJOXiOi4.js} +2 -2
  8. package/dist/{EpubViewer-hl68h4gG.js.map → EpubViewer-DJOXiOi4.js.map} +1 -1
  9. package/dist/{IpynbViewer-lhwOjXK7.js → IpynbViewer-D16jXVJc.js} +2 -2
  10. package/dist/{IpynbViewer-lhwOjXK7.js.map → IpynbViewer-D16jXVJc.js.map} +1 -1
  11. package/dist/{MermaidViewer-H2Z6AYXg.js → MermaidViewer-BnSaVg1L.js} +2 -2
  12. package/dist/{MermaidViewer-H2Z6AYXg.js.map → MermaidViewer-BnSaVg1L.js.map} +1 -1
  13. package/dist/{PsdViewer-Bsk-11yH.js → PsdViewer-BUerXaQA.js} +2 -2
  14. package/dist/{PsdViewer-Bsk-11yH.js.map → PsdViewer-BUerXaQA.js.map} +1 -1
  15. package/dist/{TiffViewer-D-j9vycG.js → TiffViewer-D0Qn3IV-.js} +2 -2
  16. package/dist/{TiffViewer-D-j9vycG.js.map → TiffViewer-D0Qn3IV-.js.map} +1 -1
  17. package/dist/{Viewer3D-Bcr70rTe.js → Viewer3D-BtRK-k1B.js} +2 -2
  18. package/dist/{Viewer3D-Bcr70rTe.js.map → Viewer3D-BtRK-k1B.js.map} +1 -1
  19. package/dist/filex-core.js +66 -50
  20. package/dist/filex-core.umd.cjs +35 -35
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-CGNE1bSv.js +11802 -0
  23. package/dist/index-CGNE1bSv.js.map +1 -0
  24. package/dist/index.d.ts +184 -0
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +416 -15
  28. package/src/components/EncryptedFolderModal.vue +125 -0
  29. package/src/components/GalleryView.vue +1 -0
  30. package/src/components/GridView.vue +1 -0
  31. package/src/components/ListView.vue +1 -0
  32. package/src/components/SecondaryPane.vue +72 -78
  33. package/src/components/Toolbar.vue +112 -3
  34. package/src/composables/useFileApi.ts +7 -0
  35. package/src/composables/useKeyboardShortcuts.ts +6 -0
  36. package/src/composables/useTabs.ts +11 -2
  37. package/src/index.ts +21 -0
  38. package/src/lib/e2ecrypto.ts +299 -0
  39. package/src/locales/en.ts +34 -0
  40. package/src/locales/tr.ts +34 -0
  41. package/src/modals/NewFolderModal.vue +18 -0
  42. package/src/modals/PreviewModal.vue +7 -2
  43. package/src/styles/base.css +126 -1
  44. package/src/styles/variables.css +2 -2
  45. package/src/types/FileNode.ts +3 -0
  46. package/dist/index-C_ZSnk_I.js +0 -11109
  47. package/dist/index-C_ZSnk_I.js.map +0 -1
@@ -0,0 +1,125 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * EncryptedFolderModal — create an E2E-encrypted folder (wiring:e2).
4
+ *
5
+ * Collects folder name + password ×2 and an explicit "I understand there
6
+ * is NO recovery" acknowledgement. The parent (FileExplorer) performs the
7
+ * actual newfolder + marker-upload dance; this modal never touches the
8
+ * network and never stores the password anywhere.
9
+ *
10
+ * Crypto scheme + threat model: docs/E2E-ENCRYPTION.md.
11
+ */
12
+ import { ref, watch } from 'vue';
13
+ import type { LocaleCode } from '../types/ExplorerConfig';
14
+ import { useLocale } from '../composables/useLocale';
15
+ import { E2E_MIN_PASSWORD_LEN } from '../lib/e2ecrypto';
16
+ import Modal from '../modals/Modal.vue';
17
+
18
+ const props = defineProps<{
19
+ open: boolean;
20
+ locale: LocaleCode;
21
+ /** True while the parent is creating the folder + uploading the marker. */
22
+ busy?: boolean;
23
+ }>();
24
+
25
+ const emit = defineEmits<{
26
+ (e: 'close'): void;
27
+ (e: 'submit', payload: { name: string; password: string }): void;
28
+ }>();
29
+
30
+ const { t } = useLocale(() => props.locale);
31
+ const name = ref('');
32
+ const password = ref('');
33
+ const password2 = ref('');
34
+ const ack = ref(false);
35
+ const err = ref<string | null>(null);
36
+
37
+ watch(
38
+ () => props.open,
39
+ (v) => {
40
+ if (v) {
41
+ name.value = '';
42
+ password.value = '';
43
+ password2.value = '';
44
+ ack.value = false;
45
+ err.value = null;
46
+ }
47
+ },
48
+ );
49
+
50
+ function submit() {
51
+ if (props.busy) return;
52
+ const clean = name.value.trim();
53
+ if (!clean) {
54
+ err.value = t('modal.newfolder.placeholder');
55
+ return;
56
+ }
57
+ if (/[\\/]/.test(clean) || clean === '.' || clean === '..' || clean.startsWith('.filex')) {
58
+ err.value = t('e2e.create.bad_name');
59
+ return;
60
+ }
61
+ if (password.value.length < E2E_MIN_PASSWORD_LEN) {
62
+ err.value = t('e2e.create.pw_short');
63
+ return;
64
+ }
65
+ if (password.value !== password2.value) {
66
+ err.value = t('e2e.create.pw_mismatch');
67
+ return;
68
+ }
69
+ if (!ack.value) {
70
+ err.value = t('e2e.create.ack_required');
71
+ return;
72
+ }
73
+ err.value = null;
74
+ emit('submit', { name: clean, password: password.value });
75
+ }
76
+ </script>
77
+
78
+ <template>
79
+ <Modal :open="open" :title="t('e2e.create.title')" size="sm" @close="emit('close')">
80
+ <form class="fe-e2e-form" @submit.prevent="submit">
81
+ <input
82
+ v-model="name"
83
+ type="text"
84
+ class="fe-input"
85
+ :placeholder="t('modal.newfolder.placeholder')"
86
+ autocomplete="off"
87
+ :disabled="busy"
88
+ />
89
+ <input
90
+ v-model="password"
91
+ type="password"
92
+ class="fe-input"
93
+ :placeholder="t('e2e.create.pw_placeholder')"
94
+ autocomplete="new-password"
95
+ :disabled="busy"
96
+ />
97
+ <input
98
+ v-model="password2"
99
+ type="password"
100
+ class="fe-input"
101
+ :placeholder="t('e2e.create.pw2_placeholder')"
102
+ autocomplete="new-password"
103
+ :disabled="busy"
104
+ @keydown.enter.prevent="submit"
105
+ />
106
+ <div class="fe-e2e-warn" role="alert">
107
+ <strong>{{ t('e2e.create.warn_title') }}</strong>
108
+ <p>{{ t('e2e.create.warn_body') }}</p>
109
+ </div>
110
+ <label class="fe-e2e-ack">
111
+ <input v-model="ack" type="checkbox" :disabled="busy" />
112
+ <span>{{ t('e2e.create.ack') }}</span>
113
+ </label>
114
+ <p v-if="err" class="fe-form__error">{{ err }}</p>
115
+ </form>
116
+ <template #actions>
117
+ <button type="button" class="fe-btn" :disabled="busy" @click="emit('close')">
118
+ {{ t('modal.newfolder.cancel') }}
119
+ </button>
120
+ <button type="button" class="fe-btn fe-btn--primary" :disabled="busy" @click="submit">
121
+ {{ busy ? t('e2e.create.busy') : t('e2e.create.create') }}
122
+ </button>
123
+ </template>
124
+ </Modal>
125
+ </template>
@@ -127,6 +127,7 @@ function parentDir(path: string): string {
127
127
  function specialEmojiFor(n: FileNode): string | null {
128
128
  if (n.basename === '.trash') return '🗑';
129
129
  if (n.mime_type === 'inode/storage') return '💾';
130
+ if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
130
131
  return null;
131
132
  }
132
133
 
@@ -126,6 +126,7 @@ function parentDir(path: string): string {
126
126
  function specialEmojiFor(n: FileNode): string | null {
127
127
  if (n.basename === '.trash') return '🗑';
128
128
  if (n.mime_type === 'inode/storage') return '💾';
129
+ if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
129
130
  return null;
130
131
  }
131
132
 
@@ -139,6 +139,7 @@ function cancelPress() {
139
139
  function specialEmojiFor(n: FileNode): string | null {
140
140
  if (n.basename === '.trash') return '🗑';
141
141
  if (n.mime_type === 'inode/storage') return '💾';
142
+ if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
142
143
  return null;
143
144
  }
144
145
 
@@ -18,10 +18,12 @@
18
18
  */
19
19
  import { computed, ref } from 'vue';
20
20
  import type { FileApi } from '../composables/useFileApi';
21
- import type { FileNode } from '../types/FileNode';
21
+ import type { FileNode, ViewMode } from '../types/FileNode';
22
22
  import type { LocaleCode } from '../types/ExplorerConfig';
23
23
  import { useLocale } from '../composables/useLocale';
24
- import { fileIconSvg } from '../lib/fileIcons';
24
+ import ListView from './ListView.vue';
25
+ import GridView from './GridView.vue';
26
+ import GalleryView from './GalleryView.vue';
25
27
 
26
28
  // Same literals ListView/GridView already hardcode for the internal DnD
27
29
  // channel; `-src` carries the origin directory so the host can move (and
@@ -50,6 +52,11 @@ const props = defineProps<{
50
52
  virtualRows?: () => FileNode[];
51
53
  /** Active-panel highlight (keyboard target). */
52
54
  active?: boolean;
55
+ /** ui-fix — the pane renders the SAME view components as the main
56
+ * panel (list/grid/gallery) instead of its own flat list. */
57
+ viewMode?: ViewMode;
58
+ /** ui-fix — authenticated thumb resolver, forwarded to grid/gallery. */
59
+ thumbSrc?: (n: FileNode) => string | null;
53
60
  }>();
54
61
 
55
62
  const emit = defineEmits<{
@@ -60,7 +67,7 @@ const emit = defineEmits<{
60
67
  (e: 'transfer', p: { sources: string[]; targetWire: string; originWire?: string }): void;
61
68
  }>();
62
69
 
63
- const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
70
+ const { t } = useLocale(() => props.locale);
64
71
 
65
72
  const path = ref<string>('');
66
73
  const files = ref<FileNode[]>([]);
@@ -149,35 +156,40 @@ function crumbGo(target: string) {
149
156
  // Selection + navigation
150
157
  // ------------------------------------------------------------------
151
158
 
152
- function onRowClick(n: FileNode, ev: MouseEvent) {
159
+ /* ui-fix adapter for the shared view components' click contract
160
+ * ({ctrl, shift} mod object instead of a MouseEvent). Shift behaves as
161
+ * ctrl here: the pane keeps a simple Set, no range anchor. */
162
+ function onViewClick(n: FileNode, mod: { ctrl: boolean; shift: boolean }) {
153
163
  emit('activate');
154
- const multi = ev.ctrlKey || ev.metaKey;
164
+ const multi = mod.ctrl || mod.shift;
155
165
  const next = new Set<string>(multi ? selected.value : []);
156
166
  if (multi && next.has(n.path)) next.delete(n.path);
157
167
  else next.add(n.path);
158
168
  selected.value = next;
159
169
  }
160
170
 
161
- function onRowDbl(n: FileNode) {
162
- if (n.type !== 'dir') return;
163
- void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
171
+ /* ui-fix — right-click on a pane item: activate + select it, no menu
172
+ * (the pane has no item menu; letting the main panel's menu open here
173
+ * would act on the WRONG panel's selection). */
174
+ function onViewContext(n: FileNode, ev: MouseEvent) {
175
+ ev.preventDefault();
176
+ ev.stopPropagation();
177
+ emit('activate');
178
+ if (!selected.value.has(n.path)) selected.value = new Set([n.path]);
164
179
  }
165
180
 
166
- // Middle-button mousedown must be cancelled on rows: in a scrollable body
167
- // Chromium's autoscroll takes over and auxclick is never generated.
168
- function onRowMiddleDown(ev: MouseEvent) {
169
- if (ev.button === 1) ev.preventDefault();
181
+ /* ui-fix drop-into from the shared views: same payload path as the
182
+ * old flat rows (dir check + wire-qualified row paths). */
183
+ function onViewDropInto(target: FileNode, ev: DragEvent) {
184
+ dropBg.value = false;
185
+ if (target.type !== 'dir' || isStorageRow(target)) return;
186
+ if (!acceptDrag(ev)) return;
187
+ handleDropPayload(ev, target.path);
170
188
  }
171
189
 
172
- // Middle-click on a folder opens it in a NEW TAB (same convention as the
173
- // main listing). stopPropagation keeps the host's delegated auxclick
174
- // listener from double-handling it.
175
- function onRowAux(n: FileNode, ev: MouseEvent) {
176
- if (ev.button !== 1) return;
177
- ev.preventDefault();
178
- ev.stopPropagation();
190
+ function onRowDbl(n: FileNode) {
179
191
  if (n.type !== 'dir') return;
180
- emit('open-tab', isStorageRow(n) ? n.path : props.toUser(n.path));
192
+ void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
181
193
  }
182
194
 
183
195
  function clearSelection() {
@@ -212,7 +224,6 @@ function onRowDragStart(n: FileNode, ev: DragEvent) {
212
224
  // Drop target (dir rows + pane background)
213
225
  // ------------------------------------------------------------------
214
226
 
215
- const dropPath = ref<string | null>(null);
216
227
  const dropBg = ref(false);
217
228
 
218
229
  function acceptDrag(ev: DragEvent): boolean {
@@ -238,28 +249,6 @@ function handleDropPayload(ev: DragEvent, targetWire: string) {
238
249
  emit('transfer', { sources, targetWire, originWire: origin });
239
250
  }
240
251
 
241
- function onRowDragOver(n: FileNode, ev: DragEvent) {
242
- if (n.type !== 'dir' || isStorageRow(n)) return;
243
- if (!acceptDrag(ev)) return;
244
- ev.preventDefault();
245
- ev.stopPropagation();
246
- if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
247
- dropPath.value = n.path;
248
- dropBg.value = false;
249
- }
250
-
251
- function onRowDragLeave(n: FileNode) {
252
- if (dropPath.value === n.path) dropPath.value = null;
253
- }
254
-
255
- function onRowDrop(n: FileNode, ev: DragEvent) {
256
- dropPath.value = null;
257
- dropBg.value = false;
258
- if (n.type !== 'dir' || isStorageRow(n)) return;
259
- if (!acceptDrag(ev)) return;
260
- handleDropPayload(ev, n.path); // real rows are wire-qualified
261
- }
262
-
263
252
  function onBgDragOver(ev: DragEvent) {
264
253
  if (!acceptDrag(ev) || atVirtualRoot.value) return;
265
254
  ev.preventDefault();
@@ -274,7 +263,6 @@ function onBgDragLeave() {
274
263
 
275
264
  function onBgDrop(ev: DragEvent) {
276
265
  dropBg.value = false;
277
- dropPath.value = null;
278
266
  if (!acceptDrag(ev) || atVirtualRoot.value) return;
279
267
  handleDropPayload(ev, props.qualify(path.value));
280
268
  }
@@ -313,10 +301,6 @@ function getPath(): string {
313
301
  }
314
302
 
315
303
  defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
316
-
317
- function specialEmojiFor(n: FileNode): string | null {
318
- return isStorageRow(n) ? '💾' : null;
319
- }
320
304
  </script>
321
305
 
322
306
  <template>
@@ -376,36 +360,46 @@ function specialEmojiFor(n: FileNode): string | null {
376
360
  <div v-else-if="files.length === 0" class="fe-split__state">
377
361
  {{ t('empty.folder') }}
378
362
  </div>
379
- <div v-else class="fe-split__list" role="listbox" aria-multiselectable="true">
380
- <div
381
- v-for="n in files"
382
- :key="n.path"
383
- class="fe-split__row"
384
- :class="{
385
- 'is-selected': selected.has(n.path),
386
- 'is-dir': n.type === 'dir',
387
- 'is-droptarget': dropPath === n.path,
388
- }"
389
- role="option"
390
- :aria-selected="selected.has(n.path) ? 'true' : 'false'"
391
- tabindex="0"
392
- draggable="true"
393
- @click="onRowClick(n, $event)"
394
- @dblclick="onRowDbl(n)"
395
- @mousedown="onRowMiddleDown($event)"
396
- @auxclick="onRowAux(n, $event)"
397
- @dragstart="onRowDragStart(n, $event)"
398
- @dragover="onRowDragOver(n, $event)"
399
- @dragleave="onRowDragLeave(n)"
400
- @drop="onRowDrop(n, $event)"
401
- >
402
- <span v-if="specialEmojiFor(n)" class="fe-split__icon" aria-hidden="true">{{ specialEmojiFor(n) }}</span>
403
- <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
404
- <span v-else class="fe-split__icon" aria-hidden="true" v-html="fileIconSvg(n)"></span>
405
- <span class="fe-split__name" :title="n.basename">{{ nodeDisplayName(n) }}</span>
406
- <span class="fe-split__size">{{ n.type === 'dir' ? '' : formatSize(n.size) }}</span>
407
- </div>
408
- </div>
363
+ <!-- ui-fix aynı görünüm bileşenleri (list/grid/gallery), pane'in
364
+ kendi viewMode'uyla; eski düz fe-split__list kalktı. -->
365
+ <ListView
366
+ v-else-if="(viewMode ?? 'list') === 'list'"
367
+ :files="files"
368
+ :selected="selected"
369
+ :locale="locale"
370
+ :loading="loading"
371
+ @click-row="onViewClick"
372
+ @dbl-row="onRowDbl"
373
+ @context-row="onViewContext"
374
+ @item-drag-start="onRowDragStart"
375
+ @item-drop-into="onViewDropInto"
376
+ />
377
+ <GridView
378
+ v-else-if="viewMode === 'grid'"
379
+ :files="files"
380
+ :selected="selected"
381
+ :locale="locale"
382
+ :loading="loading"
383
+ :thumb-src="thumbSrc"
384
+ @click-card="onViewClick"
385
+ @dbl-card="onRowDbl"
386
+ @context-card="onViewContext"
387
+ @item-drag-start="onRowDragStart"
388
+ @item-drop-into="onViewDropInto"
389
+ />
390
+ <GalleryView
391
+ v-else
392
+ :files="files"
393
+ :selected="selected"
394
+ :locale="locale"
395
+ :loading="loading"
396
+ :thumb-src="thumbSrc"
397
+ @click-card="onViewClick"
398
+ @dbl-card="onRowDbl"
399
+ @context-card="onViewContext"
400
+ @item-drag-start="onRowDragStart"
401
+ @item-drop-into="onViewDropInto"
402
+ />
409
403
  </div>
410
404
  </section>
411
405
  </template>
@@ -152,6 +152,74 @@ const mode = computed<SelectionMode>(() => props.selectionMode ?? 'none');
152
152
  // context menu uses, the two menus are guaranteed to match.
153
153
  const toolbarItems = computed(() => props.actions.filter((a) => !a.divider && !a.hidden));
154
154
 
155
+ /* === ui-fix — wide-mode action folding ===============================
156
+ * Long selection-action rows used to wrap the whole toolbar and push the
157
+ * search / view controls onto a second line. The row is now single-line:
158
+ * actions that do not fit fold into a "⋯" menu. Widths come from a
159
+ * hidden measurement strip (all actions always rendered there), so the
160
+ * calculation never mutates what it measures. */
161
+ const primaryEl = ref<HTMLElement | null>(null);
162
+ const measureEl = ref<HTMLElement | null>(null);
163
+ const wideMoreBtnEl = ref<HTMLElement | null>(null);
164
+ const wideMoreRef = ref<InstanceType<typeof ContextMenu> | null>(null);
165
+ const visibleActionCount = ref(Number.MAX_SAFE_INTEGER);
166
+ const visibleToolbarItems = computed(() => toolbarItems.value.slice(0, visibleActionCount.value));
167
+ const overflowToolbarItems = computed(() => toolbarItems.value.slice(visibleActionCount.value));
168
+ const WIDE_MORE_BTN_W = 40;
169
+
170
+ function recalcFold() {
171
+ if (props.narrow) return;
172
+ const cont = primaryEl.value;
173
+ const meas = measureEl.value;
174
+ if (!cont || !meas) {
175
+ visibleActionCount.value = toolbarItems.value.length;
176
+ return;
177
+ }
178
+ const gap = parseFloat(getComputedStyle(cont).gap) || 8;
179
+ let fixed = 0;
180
+ for (const child of Array.from(cont.children) as HTMLElement[]) {
181
+ if (child === meas || child === wideMoreBtnEl.value) continue;
182
+ if (child.classList.contains('fe-btn--fold')) continue;
183
+ if (child.offsetWidth > 0) fixed += child.offsetWidth + gap;
184
+ }
185
+ const widths = (Array.from(meas.children) as HTMLElement[]).map((c) => c.offsetWidth + gap);
186
+ const total = widths.reduce((s, w) => s + w, 0);
187
+ const avail = cont.clientWidth - fixed;
188
+ if (total <= avail) {
189
+ visibleActionCount.value = widths.length;
190
+ return;
191
+ }
192
+ let used = WIDE_MORE_BTN_W + gap;
193
+ let count = 0;
194
+ for (const w of widths) {
195
+ if (used + w > avail) break;
196
+ used += w;
197
+ count += 1;
198
+ }
199
+ visibleActionCount.value = count;
200
+ }
201
+
202
+ let foldRo: ResizeObserver | undefined;
203
+ onMounted(() => {
204
+ if (typeof ResizeObserver !== 'undefined' && primaryEl.value) {
205
+ foldRo = new ResizeObserver(() => recalcFold());
206
+ foldRo.observe(primaryEl.value);
207
+ }
208
+ void nextTick(recalcFold);
209
+ });
210
+ onBeforeUnmount(() => foldRo?.disconnect());
211
+ watch(
212
+ () => [toolbarItems.value, props.narrow, props.trashActive, mode.value] as const,
213
+ () => void nextTick(recalcFold),
214
+ { deep: false },
215
+ );
216
+
217
+ function openWideMore() {
218
+ const r = wideMoreBtnEl.value?.getBoundingClientRect();
219
+ wideMoreRef.value?.show({ clientX: r ? r.right : 0, clientY: r ? r.bottom + 4 : 0 } as MouseEvent, []);
220
+ }
221
+ /* === /ui-fix ========================================================= */
222
+
155
223
  function fire(key: string) {
156
224
  emit('action', key);
157
225
  }
@@ -291,7 +359,7 @@ function onMoreSelect(a: ContextAction) {
291
359
  >
292
360
  <!-- bag:b4 — wide layout, untouched; renders exactly as before when not narrow -->
293
361
  <template v-if="!narrow">
294
- <div class="fe-toolbar__primary">
362
+ <div ref="primaryEl" class="fe-toolbar__primary">
295
363
  <button
296
364
  v-if="canGoUp"
297
365
  type="button"
@@ -315,10 +383,10 @@ function onMoreSelect(a: ContextAction) {
315
383
  </button>
316
384
 
317
385
  <button
318
- v-for="a in toolbarItems"
386
+ v-for="a in visibleToolbarItems"
319
387
  :key="a.key"
320
388
  type="button"
321
- class="fe-btn"
389
+ class="fe-btn fe-btn--fold"
322
390
  :class="{ 'fe-btn--danger': a.danger, 'is-disabled': a.disabled }"
323
391
  :disabled="a.disabled"
324
392
  :title="a.label"
@@ -328,6 +396,36 @@ function onMoreSelect(a: ContextAction) {
328
396
  <span class="fe-btn__label">{{ a.label }}</span>
329
397
  </button>
330
398
 
399
+ <!-- ui-fix — sığmayan aksiyonlar tek satırı korumak için ⋯ menüsüne
400
+ katlanır (arama/görünüm kontrolleri artık alt satıra düşmez). -->
401
+ <button
402
+ v-if="overflowToolbarItems.length > 0"
403
+ ref="wideMoreBtnEl"
404
+ type="button"
405
+ class="fe-btn fe-btn--icon-only"
406
+ :title="t('toolbar.more')"
407
+ :aria-label="t('toolbar.more')"
408
+ aria-haspopup="menu"
409
+ @click="openWideMore"
410
+ >
411
+ <span class="fe-icon">⋯</span>
412
+ </button>
413
+
414
+ <!-- Görünmez ölçüm şeridi: TÜM aksiyonlar her zaman burada render
415
+ edilir; katlama hesabı gerçek genişliklerden yapılır. -->
416
+ <div ref="measureEl" class="fe-toolbar__measure" aria-hidden="true">
417
+ <button
418
+ v-for="a in toolbarItems"
419
+ :key="'m-' + a.key"
420
+ type="button"
421
+ class="fe-btn"
422
+ tabindex="-1"
423
+ >
424
+ <span class="fe-icon">{{ a.icon }}</span>
425
+ <span class="fe-btn__label">{{ a.label }}</span>
426
+ </button>
427
+ </div>
428
+
331
429
  <button
332
430
  v-if="pasteEnabled && mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
333
431
  type="button"
@@ -589,5 +687,16 @@ function onMoreSelect(a: ContextAction) {
589
687
  @select="onMoreSelect"
590
688
  />
591
689
  </template>
690
+
691
+ <!-- ui-fix — wide modda katlanan aksiyonların ⋯ menüsü -->
692
+ <ContextMenu
693
+ v-if="!narrow"
694
+ ref="wideMoreRef"
695
+ :locale="locale"
696
+ :theme="theme || 'auto'"
697
+ :sheet="false"
698
+ :actions="overflowToolbarItems"
699
+ @select="(a) => fire(a.key)"
700
+ />
592
701
  </div>
593
702
  </template>
@@ -55,6 +55,13 @@ export interface ManagerResponse {
55
55
  /** RBAC effective level for the current user on this directory ('' when ACL
56
56
  * is not enforced on the storage). Gates the folder-level write actions. */
57
57
  perm?: 'none' | 'viewer' | 'editor' | 'owner';
58
+ /* wiring:e2 — E2E-encrypted folder awareness: `e2e` is true when the listed
59
+ * dir IS an encrypted root; `e2e_root` is the adapter-qualified path of the
60
+ * nearest encrypted root covering this dir (set for the root itself AND for
61
+ * every subfolder inside the subtree). Absent on plain folders / old
62
+ * backends — consumers must stay undefined-safe. */
63
+ e2e?: boolean;
64
+ e2e_root?: string;
58
65
  files: FileNode[];
59
66
  }
60
67
 
@@ -341,6 +341,12 @@ export function useKeyboardShortcuts(rootEl: Ref<HTMLElement | null>, handlers:
341
341
  const root = rootEl.value;
342
342
  if (!root) return;
343
343
 
344
+ /* ui-fix — while a context menu is open, global shortcuts stay quiet
345
+ * (except Esc, which the menu scopes itself). Otherwise e.g. Delete
346
+ * opens a modal UNDER the menu backdrop and the UI wedges: the
347
+ * backdrop intercepts every click on the new dialog. */
348
+ if (e.key !== 'Escape' && document.querySelector('.fe-ctx-backdrop')) return;
349
+
344
350
  // Skip when the event originates inside a form control — don't
345
351
  // want `Delete` while editing a filename, `/` while typing in the
346
352
  // search box, etc. Escape always goes through so modals can close.
@@ -30,6 +30,9 @@ import type { ViewMode } from '../types/FileNode';
30
30
  /** Per-tab split state — the secondary pane's own location. */
31
31
  export interface TabSplit {
32
32
  path: string;
33
+ /** ui-fix — the pane's OWN view mode (list/grid/gallery); undefined
34
+ * inherits the main panel's mode at split time. */
35
+ viewMode?: ViewMode;
33
36
  }
34
37
 
35
38
  export interface TabState {
@@ -82,10 +85,16 @@ export function useTabs(opts: UseTabsOptions) {
82
85
  // gallery wave) must survive a round-trip through an older schema.
83
86
  const vm =
84
87
  typeof t.viewMode === 'string' && t.viewMode ? (t.viewMode as ViewMode) : 'list';
85
- const rawSplit = t.split as { path?: unknown } | null | undefined;
88
+ const rawSplit = t.split as { path?: unknown; viewMode?: unknown } | null | undefined;
86
89
  const split =
87
90
  rawSplit && typeof rawSplit === 'object' && typeof rawSplit.path === 'string'
88
- ? { path: rawSplit.path }
91
+ ? {
92
+ path: rawSplit.path,
93
+ viewMode:
94
+ typeof rawSplit.viewMode === 'string' && rawSplit.viewMode
95
+ ? (rawSplit.viewMode as ViewMode)
96
+ : undefined,
97
+ }
89
98
  : null;
90
99
  clean.push({
91
100
  id: typeof t.id === 'string' && t.id ? t.id : makeId(),
package/src/index.ts CHANGED
@@ -133,3 +133,24 @@ export type { TabState, TabSplit, TabsApi } from './composables/useTabs';
133
133
  export { default as TabBar } from './components/TabBar.vue';
134
134
  export { default as SecondaryPane } from './components/SecondaryPane.vue';
135
135
  /* /wiring:d1 */
136
+ /* wiring:e2 — uçtan uca şifreli klasörler (WebCrypto; docs/E2E-ENCRYPTION.md) */
137
+ export {
138
+ E2E_MARKER_NAME,
139
+ E2E_MAGIC,
140
+ E2E_VERSION,
141
+ E2E_DEFAULT_ITERATIONS,
142
+ E2E_MAX_FILE_BYTES,
143
+ E2E_MIN_PASSWORD_LEN,
144
+ E2eDecryptError,
145
+ deriveKek,
146
+ createMarker,
147
+ parseMarker,
148
+ verifyPassword,
149
+ hasMagic,
150
+ encryptFile,
151
+ decryptFile,
152
+ createKeyRing,
153
+ } from './lib/e2ecrypto';
154
+ export type { E2eMarker, E2eKeyRing } from './lib/e2ecrypto';
155
+ export { default as EncryptedFolderModal } from './components/EncryptedFolderModal.vue';
156
+ /* /wiring:e2 */